//// Encoder-level checks for foreign replay and omitted Copilot fields. //// Foreign replay items are internal typed values; these assertions also //// distinguish omitted fields from fields the permissive fake server accepts. import albedo/harness/extensions/github_copilot/models import albedo/harness/extensions/github_copilot/wire import albedo/openai_api as openai import albedo/openai_api/request as openai_request import albedo/openai_api/types import gleam/dynamic/decode import gleam/json import gleam/list import gleam/option.{type Option, Some} import gleam/result import gleam/string_tree import gleeunit/should const endpoint = "https://api.githubcopilot.com" /// The account client the extension builds, without a sign-in: the wire never /// resolves a credential itself, so a fixed key is all it takes. fn client() -> types.Client { let base = openai.client(types.ChatCompletions, endpoint, "test-token") types.Client(..base, policy: types.GitHubCopilot) } fn encode( route: models.Route, request: types.Request, ) -> Result(openai.Exchange, types.Error) { wire.encode(client(), route, request) } fn body(exchange: openai.Exchange) -> String { string_tree.to_string(exchange.body) } fn read( encoded: String, decoder: decode.Decoder(a), ) -> Result(a, json.DecodeError) { json.parse(encoded, decoder) } /// Whether `path` is missing from `encoded` altogether, which is what Copilot /// needs of a field it does not document. fn absent(encoded: String, path: List(String)) -> Bool { read(encoded, decode.at(path, decode.dynamic)) |> result.is_error } /// The `strict` key of every tool schema, refused by Copilot, so a body that /// carries one for any tool does not decode. fn tool_strict(encoded: String) -> Result(List(Bool), json.DecodeError) { read( encoded, decode.at( ["tools"], decode.list(decode.at(["function", "strict"], decode.bool)), ), ) } fn header(exchange: openai.Exchange, name: String) -> Option(String) { exchange.headers |> list.find(fn(pair) { pair.0 == name }) |> result.map(fn(pair) { pair.1 }) |> option.from_result } fn chat_item(raw: String) -> types.ReplayItem { let assert Ok(item) = json.parse(raw, types.replay_decoder(types.ChatCompletions)) item } fn responses_item(raw: String) -> types.ReplayItem { let assert Ok(item) = json.parse(raw, types.replay_decoder(types.Responses)) item } fn picture() -> types.Image { let assert Ok(image) = types.image("image/png", "aGVsbG8=", 2, 3, 5) image } /// One message of an encoded Chat Completions body, with the two foreign /// reasoning fields reported as presence: Copilot must see neither. type WireMessage { WireMessage( role: String, content: String, tool_call_id: String, calls: List(types.ToolCall), reasoning_content: Bool, reasoning_details: Bool, ) } fn wire_message() -> decode.Decoder(WireMessage) { use role <- decode.field("role", decode.string) // A turn that was only a tool call carries `null` content. use content <- decode.optional_field( "content", "", decode.map(decode.optional(decode.string), option.unwrap(_, "")), ) use tool_call_id <- decode.optional_field("tool_call_id", "", decode.string) use calls <- decode.optional_field("tool_calls", [], decode.list(chat_call())) use reasoning_content <- decode.optional_field( "reasoning_content", False, decode.map(decode.dynamic, fn(_) { True }), ) use reasoning_details <- decode.optional_field( "reasoning_details", False, decode.map(decode.dynamic, fn(_) { True }), ) decode.success(WireMessage( role, content, tool_call_id, calls, reasoning_content, reasoning_details, )) } fn chat_call() -> decode.Decoder(types.ToolCall) { use id <- decode.field("id", decode.string) use name <- decode.subfield(["function", "name"], decode.string) use arguments <- decode.subfield(["function", "arguments"], decode.string) decode.success(types.ToolCall(id, name, arguments)) } fn messages(encoded: String) -> Result(List(WireMessage), json.DecodeError) { read(encoded, decode.at(["messages"], decode.list(wire_message()))) } pub fn chat_replay_drops_foreign_reasoning_and_keeps_text_and_calls_test() -> Nil { let assistant = chat_item( "{\"role\":\"assistant\",\"content\":\"reading\",\"reasoning_content\":\"hm\"," <> "\"reasoning_details\":[{\"type\":\"text\",\"text\":\"hm\"}]," <> "\"tool_calls\":[{\"id\":\"call_1\",\"type\":\"function\"," <> "\"function\":{\"name\":\"read\",\"arguments\":\"{\\\"path\\\":\\\"a\\\"}\"}}," <> "{\"id\":\"call_2\",\"type\":\"function\"," <> "\"function\":{\"name\":\"write\",\"arguments\":\"{}\"}}]}", ) let request = openai.request("gpt-5.1", [ types.User("go"), types.Replay(assistant), types.ToolOutput("call_1", "file text", []), types.ToolOutput("call_2", "written", []), ]) let assert Ok(exchange) = encode(models.ChatRoute, request) let assert Ok([ask, reply, first, second]) = messages(body(exchange)) should.equal(ask.role, "user") should.equal(reply.role, "assistant") should.equal(reply.content, "reading") should.equal(reply.calls, [ types.ToolCall("call_1", "read", "{\"path\":\"a\"}"), types.ToolCall("call_2", "write", "{}"), ]) should.be_false(reply.reasoning_content) should.be_false(reply.reasoning_details) should.equal(#(first.role, first.tool_call_id, first.content), #( "tool", "call_1", "file text", )) should.equal(#(second.role, second.tool_call_id, second.content), #( "tool", "call_2", "written", )) // A refused turn's words are the only text it has; dropping them would send // an empty assistant message where the model refused to answer. let refused = chat_item( "{\"role\":\"assistant\",\"content\":\"\",\"refusal\":\"not doing that\"}", ) let assert Ok(refused_exchange) = encode( models.ChatRoute, openai.request("gpt-5.1", [types.User("go"), types.Replay(refused)]), ) let assert Ok([_, refusal]) = messages(body(refused_exchange)) should.equal(refusal.content, "not doing that") } pub fn native_responses_replay_is_refused_on_every_route_test() -> Nil { let native = responses_item( "{\"type\":\"reasoning\",\"id\":\"rs_1\",\"encrypted_content\":\"opaque\"}", ) let request = openai.request("gpt-5.1", [types.User("hi"), types.Replay(native)]) let assert Error(types.InvalidRequest(_)) = encode(models.ChatRoute, request) let assert Error(types.InvalidRequest(_)) = encode(models.ResponsesRoute, request) let assert Error(types.InvalidRequest(_)) = encode(models.MessagesRoute, request) // The same history as portable chat replay is what every route takes instead. let portable = chat_item( "{\"role\":\"assistant\",\"content\":\"hi\"," <> "\"tool_calls\":[{\"id\":\"c\",\"type\":\"function\"," <> "\"function\":{\"name\":\"read\",\"arguments\":\"{}\"}}]}", ) let portable_request = openai.request("gpt-5.1", [types.User("hi"), types.Replay(portable)]) let assert Ok(_) = encode(models.ChatRoute, portable_request) let assert Ok(_) = encode(models.ResponsesRoute, portable_request) let assert Ok(_) = encode(models.MessagesRoute, portable_request) Nil } pub fn chat_body_keeps_copilot_field_names_and_omits_strict_test() -> Nil { let request = types.Request( ..openai.request("gpt-5.1", [types.User("hi")]), tools: [types.Tool("read", "read a file", json.object([]), True)], max_output_tokens: Some(128), options: types.Options( ..types.defaults, effort: Some("high"), service_tier: Some("priority"), ), ) let assert Ok(exchange) = encode(models.ChatRoute, request) let encoded = body(exchange) // Copilot limits and bills on `max_tokens`; `max_completion_tokens` is not a // field it documents. should.equal(read(encoded, decode.at(["max_tokens"], decode.int)), Ok(128)) should.be_true(absent(encoded, ["max_completion_tokens"])) // A sampling control and a tier it does not document stay off the body // rather than travelling empty. should.be_true(absent(encoded, ["reasoning_effort"])) should.be_true(absent(encoded, ["service_tier"])) // Copilot rejects the `strict` key outright, so it is missing, not false. should.be_true(result.is_error(tool_strict(encoded))) // The shared OpenAI policy carries both: the policy above is what removes // them, not the shape of this request. let assert Ok(plain) = openai_request.encode_with_policy( types.ChatCompletions, types.OpenAI, request, ) let plain = string_tree.to_string(plain) should.equal(tool_strict(plain), Ok([True])) should.equal( read(plain, decode.at(["service_tier"], decode.string)), Ok("priority"), ) should.equal( read(plain, decode.at(["reasoning_effort"], decode.string)), Ok("high"), ) } pub fn initiator_headers_follow_the_last_input_and_vision_follows_images_test() -> Nil { let assert Ok(person) = encode(models.ChatRoute, openai.request("gpt-5.1", [types.User("hi")])) should.equal(header(person, "x-initiator"), Some("user")) should.equal(header(person, "x-interaction-type"), Some("conversation-user")) should.be_none(header(person, "copilot-vision-request")) // A tool result is the agent's follow-up, not the person's turn. let assert Ok(agent) = encode( models.ChatRoute, openai.request("gpt-5.1", [ types.User("hi"), types.ToolOutput("call_1", "out", []), ]), ) should.equal(header(agent, "x-initiator"), Some("agent")) should.equal(header(agent, "x-interaction-type"), Some("conversation-agent")) should.be_none(header(agent, "copilot-vision-request")) // An image anywhere in the request asks for the vision channel, and the // person who attached it is still the initiator. let assert Ok(vision) = encode( models.ChatRoute, openai.request("gpt-5.1", [types.UserImage("look", [picture()])]), ) should.equal(header(vision, "x-initiator"), Some("user")) should.equal(header(vision, "copilot-vision-request"), Some("true")) let assert Ok(tool_image) = encode( models.ChatRoute, openai.request("gpt-5.1", [ types.User("hi"), types.ToolOutput("call_1", "", [picture()]), ]), ) should.equal(header(tool_image, "x-initiator"), Some("agent")) should.equal(header(tool_image, "copilot-vision-request"), Some("true")) } pub fn routes_are_copilot_endpoints_and_the_account_key_is_the_only_credential_test() -> Nil { let request = openai.request("gpt-5.1", [types.User("hi")]) let assert Ok(chat) = encode(models.ChatRoute, request) should.equal(chat.url, endpoint <> "/chat/completions") should.be_none(header(chat, "anthropic-version")) let assert Ok(responses) = encode(models.ResponsesRoute, request) should.equal(responses.url, endpoint <> "/responses") should.be_none(header(responses, "anthropic-version")) let assert Ok(messages) = encode(models.MessagesRoute, request) should.equal(messages.url, endpoint <> "/v1/messages") should.equal(header(messages, "anthropic-version"), Some("2023-06-01")) // The wire sends the account's own key once and invents no other credential; // resolving it belongs to the extension, not the encoder. should.equal( list.filter(chat.headers, fn(pair) { pair.0 == "authorization" }), [#("authorization", "Bearer test-token")], ) should.equal(header(chat, "x-initiator"), Some("user")) }