copilot-relay 0.4.8 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,15 +1,16 @@
1
1
  # copilot-relay
2
2
 
3
- **Use Claude Code with the models available through your GitHub Copilot subscription.**
3
+ **Use Claude Code and Codex with the models available through your GitHub Copilot subscription.**
4
4
 
5
- [![CI](https://github.com/D0n9X1n/copilot-relay/actions/workflows/ci.yml/badge.svg)](https://github.com/D0n9X1n/copilot-relay/actions/workflows/ci.yml)
6
- [![npm version](https://img.shields.io/npm/v/copilot-relay.svg?logo=npm)](https://www.npmjs.com/package/copilot-relay)
7
- [![Node.js](https://img.shields.io/badge/node-%3E%3D22-339933?logo=node.js&logoColor=white)](https://nodejs.org/)
8
- [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
+ [![CI](https://img.shields.io/github/actions/workflow/status/D0n9X1n/copilot-relay/ci.yml?branch=main&logo=githubactions&logoColor=white&label=CI)](https://github.com/D0n9X1n/copilot-relay/actions/workflows/ci.yml) [![npm version](https://img.shields.io/npm/v/copilot-relay?logo=npm)](https://www.npmjs.com/package/copilot-relay) [![npm downloads](https://img.shields.io/npm/dm/copilot-relay?logo=npm&label=downloads)](https://www.npmjs.com/package/copilot-relay) [![GitHub release](https://img.shields.io/github/v/release/D0n9X1n/copilot-relay?logo=github)](https://github.com/D0n9X1n/copilot-relay/releases/latest)
6
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D22-339933?logo=node.js&logoColor=white)](https://nodejs.org/) [![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178C6?logo=typescript&logoColor=white)](tsconfig.json) [![Platforms](https://img.shields.io/badge/platform-macOS%20%7C%20Windows%20%7C%20Linux-lightgrey)](wiki/README.md) [![License: MIT](https://img.shields.io/github/license/D0n9X1n/copilot-relay?color=blue)](LICENSE)
9
7
 
10
8
  ## Features
11
9
 
10
+ [![Watch a one-minute video tour of copilot-relay](https://gitdiagram.com/video-badge.svg)](https://gitdiagram.com/d0n9x1n/copilot-relay/video)
11
+
12
12
  - **Claude Code, familiar workflow** — Messages API compatibility, streaming responses, and tool calls.
13
+ - **Codex CLI and the Codex app** — an OpenAI Responses passthrough for Copilot's GPT models; [set it up](wiki/EN-Codex.md) · [中文](wiki/ZH-Codex.md).
13
14
  - **WebSearch that fits the conversation** — relay-managed search, followed by an answer that can still use your tools.
14
15
  - **Your models, your settings** — configurable GPT/Opus routes and reasoning effort; existing selections survive upgrades.
15
16
  - **Use the advertised capacity** — discovered context/output limits, without silently shortening your input.
@@ -74,6 +75,5 @@ scripts. It asks GitHub with the stored token, so no relay needs to run.
74
75
  - Run at login: [macOS](wiki/EN-macOS-LaunchAgent.md) · [Windows](wiki/EN-Windows-Service.md) · [Linux](wiki/EN-Linux-systemd.md)
75
76
  - [Commands](wiki/EN-Commands.md) · [Prompt caching](wiki/EN-Prompt-Caching.md) · [Troubleshoot](wiki/EN-Logging-Troubleshooting.md) · [Understand the architecture](wiki/EN-Architecture.md) · [Contribute](wiki/EN-Development.md)
76
77
 
77
- Unofficial research project; not affiliated with GitHub or Anthropic. Upstream
78
- services and compatibility can change. Model and tool support are not guaranteed;
79
- see the Wiki for known limitations. [MIT licensed](LICENSE).
78
+ Unofficial research project; not affiliated with GitHub, Anthropic or OpenAI. Upstream services and
79
+ compatibility can change; model and tool support are not guaranteed (see the Wiki). [MIT licensed](LICENSE).
package/dist/main.js CHANGED
@@ -3343,10 +3343,10 @@ const pinCopilotModelCatalog = (config) => {
3343
3343
  pinnedCatalogs.set(config, config.modelCatalog);
3344
3344
  };
3345
3345
  const pendingCatalogs = /* @__PURE__ */ new WeakMap();
3346
- const isRecord$5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3346
+ const isRecord$6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3347
3347
  const isPositiveInteger = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0;
3348
3348
  function parseModelTokenLimits(value) {
3349
- if (!isRecord$5(value)) return;
3349
+ if (!isRecord$6(value)) return;
3350
3350
  const context = value.max_context_window_tokens;
3351
3351
  const prompt = value.max_prompt_tokens;
3352
3352
  const output = value.max_output_tokens;
@@ -3402,13 +3402,13 @@ async function loadCopilotModelCatalog(config) {
3402
3402
  });
3403
3403
  if (!response.ok) throw new HTTPError("Failed to validate upstream models", response);
3404
3404
  const payload = await readCopilotJson(response, signal, config.upstreamTimeoutMs);
3405
- if (!isRecord$5(payload) || !Array.isArray(payload.data)) throw new Error("Copilot /models returned an invalid model catalog.");
3405
+ if (!isRecord$6(payload) || !Array.isArray(payload.data)) throw new Error("Copilot /models returned an invalid model catalog.");
3406
3406
  const models$1 = /* @__PURE__ */ new Map();
3407
3407
  for (const model of payload.data) {
3408
- if (!isRecord$5(model) || typeof model.id !== "string" || !model.id) continue;
3409
- const capabilities = isRecord$5(model.capabilities) ? model.capabilities : void 0;
3408
+ if (!isRecord$6(model) || typeof model.id !== "string" || !model.id) continue;
3409
+ const capabilities = isRecord$6(model.capabilities) ? model.capabilities : void 0;
3410
3410
  const limits = parseModelTokenLimits(capabilities?.limits);
3411
- const supports = isRecord$5(capabilities?.supports) ? capabilities.supports : void 0;
3411
+ const supports = isRecord$6(capabilities?.supports) ? capabilities.supports : void 0;
3412
3412
  const supportedEndpoints = parseStringArray(model.supported_endpoints);
3413
3413
  const reasoningEfforts = parseReasoningEfforts(supports);
3414
3414
  models$1.set(model.id, {
@@ -3753,7 +3753,7 @@ const claudeCodeContext = [
3753
3753
  const claudeCodeContextPatterns = [/^\S+ hook additional context:/];
3754
3754
  const statusProbePrompt = "Reply with the single word: ok";
3755
3755
  const statusProbeMaxTokens = 16;
3756
- const isRecord$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3756
+ const isRecord$5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3757
3757
  const innermostReminder = /<system-reminder>(?:(?!<system-reminder>)[\s\S])*?<\/system-reminder>/g;
3758
3758
  const withoutReminders = (text$1) => {
3759
3759
  let previous = text$1;
@@ -3771,14 +3771,14 @@ const contentBlocks = (content) => {
3771
3771
  type: "text",
3772
3772
  text: content
3773
3773
  }];
3774
- return Array.isArray(content) ? content.filter(isRecord$4) : [];
3774
+ return Array.isArray(content) ? content.filter(isRecord$5) : [];
3775
3775
  };
3776
3776
  const originatingTurn = (messages) => {
3777
3777
  const blocks = [];
3778
3778
  let sawUser = false;
3779
3779
  for (let index = messages.length - 1; index >= 0; index--) {
3780
3780
  const message = messages[index];
3781
- if (!isRecord$4(message) || message.role === "system") continue;
3781
+ if (!isRecord$5(message) || message.role === "system") continue;
3782
3782
  if (message.role !== "user") break;
3783
3783
  sawUser = true;
3784
3784
  blocks.unshift(...contentBlocks(message.content));
@@ -3798,11 +3798,11 @@ const blockOrigin = (block) => {
3798
3798
  const isStatusProbe = (body) => {
3799
3799
  if (Object.keys(body).sort().join(",") !== "max_tokens,messages,model" || body.max_tokens !== statusProbeMaxTokens) return false;
3800
3800
  const messages = body.messages;
3801
- return Array.isArray(messages) && messages.length === 1 && isRecord$4(messages[0]) && messages[0].role === "user" && messages[0].content === statusProbePrompt;
3801
+ return Array.isArray(messages) && messages.length === 1 && isRecord$5(messages[0]) && messages[0].role === "user" && messages[0].content === statusProbePrompt;
3802
3802
  };
3803
3803
  const systemTexts = (system) => contentBlocks(system).flatMap((block) => block.type === "text" && typeof block.text === "string" ? [block.text] : []);
3804
3804
  const getClaudeRequestInitiator = (body) => {
3805
- if (!isRecord$4(body) || !Array.isArray(body.messages) || isStatusProbe(body)) return "agent";
3805
+ if (!isRecord$5(body) || !Array.isArray(body.messages) || isStatusProbe(body)) return "agent";
3806
3806
  const payload = body;
3807
3807
  if (readBillingLineField(payload, "cc_is_subagent") === "true" || readBillingLineField(payload, "cc_workload") === "cron") return "agent";
3808
3808
  if (systemTexts(removeBillingLine(payload).system).some((text$1) => startsWithAny(text$1.trimStart(), ownRequestSystemPrompts))) return "agent";
@@ -4406,7 +4406,7 @@ const webSearchInputSchema = {
4406
4406
  required: ["query"],
4407
4407
  additionalProperties: false
4408
4408
  };
4409
- const isRecord$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4409
+ const isRecord$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4410
4410
  const isAnthropicNativeWebSearchTool = (tool) => tool.name === "web_search" && typeof tool.type === "string" && anthropicWebSearchToolPattern.test(tool.type);
4411
4411
  const isClaudeCodeWebSearchTool = (tool) => tool.name === claudeCodeWebSearchToolName;
4412
4412
  const isClaudeWebSearchTool = (tool) => isAnthropicNativeWebSearchTool(tool) || isClaudeCodeWebSearchTool(tool);
@@ -4446,7 +4446,7 @@ const getQueryFromToolArguments = (value) => {
4446
4446
  if (!trimmed) return;
4447
4447
  try {
4448
4448
  const parsed = JSON.parse(trimmed);
4449
- if (isRecord$3(parsed) && typeof parsed.query === "string" && parsed.query.trim()) return parsed.query.trim();
4449
+ if (isRecord$4(parsed) && typeof parsed.query === "string" && parsed.query.trim()) return parsed.query.trim();
4450
4450
  } catch {
4451
4451
  return trimmed;
4452
4452
  }
@@ -4503,7 +4503,7 @@ const getReportedQuery = (action, queries) => {
4503
4503
  const getSearchQuery = (response, requestedQuery) => {
4504
4504
  for (const item of response.output ?? []) {
4505
4505
  if (item.type !== "web_search_call") continue;
4506
- const action = isRecord$3(item.action) ? item.action : void 0;
4506
+ const action = isRecord$4(item.action) ? item.action : void 0;
4507
4507
  const query = getReportedQuery(action, Array.isArray(action?.queries) ? action.queries : []);
4508
4508
  if (query) return query;
4509
4509
  }
@@ -4511,7 +4511,7 @@ const getSearchQuery = (response, requestedQuery) => {
4511
4511
  };
4512
4512
  const getResponseText = (response) => (response.output ?? []).flatMap((item) => {
4513
4513
  if (item.type !== "message") return [];
4514
- return (Array.isArray(item.content) ? item.content : []).flatMap((part) => isRecord$3(part) && part.type === "output_text" && typeof part.text === "string" ? [part.text] : []);
4514
+ return (Array.isArray(item.content) ? item.content : []).flatMap((part) => isRecord$4(part) && part.type === "output_text" && typeof part.text === "string" ? [part.text] : []);
4515
4515
  }).join("\n").trim();
4516
4516
  const cleanTitle = (line, url) => {
4517
4517
  return line.slice(0, line.indexOf(url)).replace(/^\s*(?:[-*]|\d+[.)])\s*/, "").replace(/\s*(?:[-:|])\s*$/, "").trim() || new URL(url).hostname;
@@ -4535,15 +4535,15 @@ const parseSearchResults = (text$1) => {
4535
4535
  const getStructuredSearchResults = (response) => {
4536
4536
  const sources = [];
4537
4537
  for (const item of response.output ?? []) {
4538
- if (item.type === "web_search_call" && isRecord$3(item.action) && Array.isArray(item.action.sources)) sources.push(...item.action.sources);
4538
+ if (item.type === "web_search_call" && isRecord$4(item.action) && Array.isArray(item.action.sources)) sources.push(...item.action.sources);
4539
4539
  if (item.type === "message" && Array.isArray(item.content)) {
4540
- for (const part of item.content) if (isRecord$3(part) && Array.isArray(part.annotations)) sources.push(...part.annotations.filter((annotation) => isRecord$3(annotation) && annotation.type === "url_citation"));
4540
+ for (const part of item.content) if (isRecord$4(part) && Array.isArray(part.annotations)) sources.push(...part.annotations.filter((annotation) => isRecord$4(annotation) && annotation.type === "url_citation"));
4541
4541
  }
4542
4542
  }
4543
4543
  const results = [];
4544
4544
  const seen = /* @__PURE__ */ new Set();
4545
4545
  for (const source of sources) {
4546
- if (!isRecord$3(source) || typeof source.url !== "string") continue;
4546
+ if (!isRecord$4(source) || typeof source.url !== "string") continue;
4547
4547
  try {
4548
4548
  const url = new URL(source.url);
4549
4549
  if (!/^https?:$/.test(url.protocol) || url.username || url.password || seen.has(url.href)) continue;
@@ -4595,21 +4595,21 @@ const getResultSource = (structured, results) => {
4595
4595
  return results.length ? "text" : "none";
4596
4596
  };
4597
4597
  const interpretSearchResponse = (raw, config, requestedQuery, request, requestedEffort, requestId) => {
4598
- const malformed = !isRecord$3(raw) || raw.output !== void 0 && (!Array.isArray(raw.output) || !raw.output.every(isRecord$3));
4599
- const upstream = isRecord$3(raw) ? {
4598
+ const malformed = !isRecord$4(raw) || raw.output !== void 0 && (!Array.isArray(raw.output) || !raw.output.every(isRecord$4));
4599
+ const upstream = isRecord$4(raw) ? {
4600
4600
  ...raw,
4601
- output: Array.isArray(raw.output) ? raw.output.filter(isRecord$3) : []
4601
+ output: Array.isArray(raw.output) ? raw.output.filter(isRecord$4) : []
4602
4602
  } : {};
4603
4603
  const status$1 = recognizedValue(upstream.status, responseStates);
4604
- const reason = recognizedValue(isRecord$3(upstream.incomplete_details) ? upstream.incomplete_details.reason : void 0, incompleteReasons);
4604
+ const reason = recognizedValue(isRecord$4(upstream.incomplete_details) ? upstream.incomplete_details.reason : void 0, incompleteReasons);
4605
4605
  const items = upstream.output ?? [];
4606
4606
  const calls = items.filter((item) => item.type === "web_search_call");
4607
4607
  const callStates = calls.map((item) => recognizedValue(item.status, searchStates));
4608
- const usage$1 = isRecord$3(upstream.usage) ? upstream.usage : {};
4608
+ const usage$1 = isRecord$4(upstream.usage) ? upstream.usage : {};
4609
4609
  const inputTokens = reportedTokens(usage$1.input_tokens);
4610
- const cachedInputTokens = reportedTokens(isRecord$3(usage$1.input_tokens_details) ? usage$1.input_tokens_details.cached_tokens : void 0);
4610
+ const cachedInputTokens = reportedTokens(isRecord$4(usage$1.input_tokens_details) ? usage$1.input_tokens_details.cached_tokens : void 0);
4611
4611
  const outputTokens = reportedTokens(usage$1.output_tokens);
4612
- const reasoningTokens = reportedTokens(isRecord$3(usage$1.output_tokens_details) ? usage$1.output_tokens_details.reasoning_tokens : void 0);
4612
+ const reasoningTokens = reportedTokens(isRecord$4(usage$1.output_tokens_details) ? usage$1.output_tokens_details.reasoning_tokens : void 0);
4613
4613
  const upstreamResponseId = safeResponseId(upstream.id, config.copilotToken);
4614
4614
  const safeRequestId = typeof requestId === "string" && /^[a-f0-9-]{36}$/i.test(requestId) ? requestId : "unreported";
4615
4615
  const text$1 = getResponseText(upstream);
@@ -4697,7 +4697,7 @@ const getSearchFailureDetail = (body, token) => {
4697
4697
  let detail = body.trim();
4698
4698
  try {
4699
4699
  const parsed = JSON.parse(detail);
4700
- detail = getErrorMessageOrCode(isRecord$3(parsed) && isRecord$3(parsed.error) ? parsed.error : void 0);
4700
+ detail = getErrorMessageOrCode(isRecord$4(parsed) && isRecord$4(parsed.error) ? parsed.error : void 0);
4701
4701
  } catch {
4702
4702
  if (/^[{[<]/.test(detail)) return "";
4703
4703
  }
@@ -5269,7 +5269,7 @@ const lookaroundPrefixes = [
5269
5269
  "(?<=",
5270
5270
  "(?<!"
5271
5271
  ];
5272
- const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
5272
+ const isRecord$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
5273
5273
  function hasUnsupportedPatternFeatures(pattern) {
5274
5274
  let inCharacterClass = false;
5275
5275
  for (let index = 0; index < pattern.length; index++) {
@@ -5315,12 +5315,12 @@ function mapArray(values) {
5315
5315
  }
5316
5316
  function normalizeSchemaValue(value) {
5317
5317
  if (Array.isArray(value)) return mapArray(value);
5318
- return isRecord$2(value) ? normalizeResponsesToolSchema(value) : value;
5318
+ return isRecord$3(value) ? normalizeResponsesToolSchema(value) : value;
5319
5319
  }
5320
5320
  function normalizeResponsesToolSchema(schema) {
5321
5321
  return mapRecord(schema, (key, value) => {
5322
5322
  if (key === "pattern" && typeof value === "string" && hasUnsupportedPatternFeatures(value)) return removed;
5323
- if (schemaMapKeywords.has(key) && isRecord$2(value)) return mapRecord(value, (_name, nested) => normalizeSchemaValue(nested));
5323
+ if (schemaMapKeywords.has(key) && isRecord$3(value)) return mapRecord(value, (_name, nested) => normalizeSchemaValue(nested));
5324
5324
  if (nestedSchemaKeywords.has(key)) return normalizeSchemaValue(value);
5325
5325
  return value;
5326
5326
  });
@@ -6897,6 +6897,320 @@ claudeRoutes.post("/messages/count_tokens", async (c) => {
6897
6897
  }
6898
6898
  });
6899
6899
 
6900
+ //#endregion
6901
+ //#region src/codex/request.ts
6902
+ const copilotReasoningEfforts = [
6903
+ "none",
6904
+ "minimal",
6905
+ "low",
6906
+ "medium",
6907
+ "high",
6908
+ "xhigh",
6909
+ "max"
6910
+ ];
6911
+ const effortOrder = [
6912
+ "none",
6913
+ "minimal",
6914
+ "low",
6915
+ "medium",
6916
+ "high",
6917
+ "xhigh",
6918
+ "max",
6919
+ "ultra"
6920
+ ];
6921
+ const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
6922
+ const codexRequestError = (message, code, status$1 = 400) => new HTTPError(message, Response.json({ error: {
6923
+ message,
6924
+ type: "invalid_request_error",
6925
+ code
6926
+ } }, { status: status$1 }), message);
6927
+ const parseCodexPayload = (text$1) => {
6928
+ let body;
6929
+ try {
6930
+ body = JSON.parse(text$1);
6931
+ } catch {
6932
+ throw codexRequestError("The request body is not valid JSON.", "invalid_json");
6933
+ }
6934
+ if (!isRecord$2(body) || typeof body.model !== "string" || body.model.trim() === "") throw codexRequestError("The request needs a model.", "missing_model");
6935
+ if (typeof body.input !== "string" && !Array.isArray(body.input)) throw codexRequestError("The request needs an input string or array.", "missing_input");
6936
+ return body;
6937
+ };
6938
+ const catalogDescribesUpstream = (config) => config.modelCatalog?.baseUrl === config.copilotBaseUrl;
6939
+ /**
6940
+ * The Copilot model a Codex request is sent to.
6941
+ *
6942
+ * A model Copilot's catalog lists is used as named, so Codex's own model IDs reach the same
6943
+ * Copilot model. A name the catalog does not list, such as Codex's internal codex-auto-review,
6944
+ * goes to the configured gptModel, as Claude Code's aliases do. Without a catalog the name is
6945
+ * forwarded unchanged. Claude models are refused: Copilot serves them only on Messages and Chat
6946
+ * Completions.
6947
+ */
6948
+ const resolveCodexModel = async (config, requested) => {
6949
+ if (isClaudeModelId(requested)) throw codexRequestError(`${requested} is not available through the Responses API: Copilot serves Claude models only on Messages and Chat Completions. Choose a GPT model.`, "relay_unsupported_model");
6950
+ const normalized = normalizeCopilotModelId(requested);
6951
+ await ensureCopilotModelCatalog(config, normalized);
6952
+ pinCopilotModelCatalog(config);
6953
+ const model = !(getCachedCopilotModel(config, normalized) !== void 0) && catalogDescribesUpstream(config) ? getModelRouting().gptModel : normalized;
6954
+ const capabilities = getCachedCopilotModel(config, model);
6955
+ const notChat = capabilities?.type !== void 0 && capabilities.type !== "chat";
6956
+ const noResponses = capabilities?.supportedEndpoints !== void 0 && !capabilities.supportedEndpoints.includes("/responses");
6957
+ if (notChat || noResponses) throw codexRequestError(`${model} does not serve the Responses API on Copilot. Choose a model that lists /responses in copilot-relay models.`, "relay_unsupported_endpoint");
6958
+ return model;
6959
+ };
6960
+ const effortRank = (effort) => effortOrder.indexOf(effort);
6961
+ /**
6962
+ * The reasoning effort to send for a requested one.
6963
+ *
6964
+ * A level the model advertises, or one this relay does not know, is sent as requested. Any other
6965
+ * level becomes the highest advertised level below it, or the lowest advertised level when none is
6966
+ * below it. A model that advertises no levels gets none, since Copilot rejects the field for it.
6967
+ */
6968
+ const clampReasoningEffort = (requested, advertised) => {
6969
+ if (advertised?.length === 0) return;
6970
+ const accepted = advertised ?? copilotReasoningEfforts;
6971
+ if (accepted.includes(requested) || effortRank(requested) === -1) return requested;
6972
+ const ranked = accepted.filter((effort) => effortRank(effort) !== -1).sort((left, right) => effortRank(left) - effortRank(right));
6973
+ return ranked.filter((effort) => effortRank(effort) <= effortRank(requested)).at(-1) ?? ranked[0];
6974
+ };
6975
+ /**
6976
+ * The body sent to Copilot for a Codex request.
6977
+ *
6978
+ * Copilot answers HTTP 400 to `service_tier`, to an `image_generation` tool, and to an effort it
6979
+ * does not accept; those are the only changes. Every other field, including Codex's
6980
+ * prompt_cache_key and its encrypted reasoning, is forwarded unchanged.
6981
+ */
6982
+ const prepareCodexPayload = (payload, model, capabilities) => {
6983
+ const prepared = {
6984
+ ...payload,
6985
+ model
6986
+ };
6987
+ const removed$1 = [];
6988
+ if (Object.hasOwn(prepared, "service_tier")) {
6989
+ delete prepared.service_tier;
6990
+ removed$1.push("service_tier");
6991
+ }
6992
+ if (Array.isArray(prepared.tools)) {
6993
+ const tools = prepared.tools.filter((tool) => !(isRecord$2(tool) && tool.type === "image_generation"));
6994
+ if (tools.length !== prepared.tools.length) {
6995
+ prepared.tools = tools;
6996
+ removed$1.push("image_generation");
6997
+ }
6998
+ }
6999
+ const reasoning = isRecord$2(prepared.reasoning) ? prepared.reasoning : void 0;
7000
+ const requestedEffort = typeof reasoning?.effort === "string" ? reasoning.effort : void 0;
7001
+ let effectiveEffort = requestedEffort;
7002
+ if (reasoning !== void 0 && requestedEffort !== void 0) {
7003
+ effectiveEffort = clampReasoningEffort(requestedEffort, capabilities?.reasoningEfforts);
7004
+ if (effectiveEffort === void 0) {
7005
+ const withoutEffort = { ...reasoning };
7006
+ delete withoutEffort.effort;
7007
+ prepared.reasoning = withoutEffort;
7008
+ } else if (effectiveEffort !== requestedEffort) prepared.reasoning = {
7009
+ ...reasoning,
7010
+ effort: effectiveEffort
7011
+ };
7012
+ }
7013
+ return {
7014
+ payload: prepared,
7015
+ removed: removed$1,
7016
+ requestedEffort,
7017
+ effectiveEffort
7018
+ };
7019
+ };
7020
+ /**
7021
+ * Who started a Codex request, for Copilot's x-initiator header.
7022
+ *
7023
+ * A request continues the agent's own work unless its last input item is a user message: after a
7024
+ * tool call the last item is the tool's output. Codex marks its subagents (review, compaction,
7025
+ * memory consolidation, spawned agents) with x-openai-subagent, and those are always the agent.
7026
+ */
7027
+ const getCodexInitiator = (input, subagent) => {
7028
+ if (subagent !== void 0) return "agent";
7029
+ if (typeof input === "string") return "user";
7030
+ const last = Array.isArray(input) ? input.at(-1) : void 0;
7031
+ return isRecord$2(last) && last.role === "user" && (last.type === void 0 || last.type === "message") ? "user" : "agent";
7032
+ };
7033
+ const includesImage = (value) => {
7034
+ if (Array.isArray(value)) return value.some(includesImage);
7035
+ if (!isRecord$2(value)) return false;
7036
+ return value.type === "input_image" || Object.values(value).some(includesImage);
7037
+ };
7038
+ /**
7039
+ * A stream holding only `response.failed` with code `context_length_exceeded`.
7040
+ *
7041
+ * Copilot reports a prompt over the model's input limit as HTTP 400, which Codex treats as a fatal
7042
+ * error. Codex recognizes a context-window overflow only from this event, and answers it by
7043
+ * compacting the conversation.
7044
+ */
7045
+ const contextOverflowStream = (requestId, message) => {
7046
+ const event = {
7047
+ type: "response.failed",
7048
+ response: {
7049
+ id: `resp_relay_${requestId}`,
7050
+ object: "response",
7051
+ status: "failed",
7052
+ error: {
7053
+ code: "context_length_exceeded",
7054
+ message
7055
+ }
7056
+ }
7057
+ };
7058
+ return `event: response.failed\ndata: ${JSON.stringify(event)}\n\n`;
7059
+ };
7060
+
7061
+ //#endregion
7062
+ //#region src/routes/responses.ts
7063
+ const responsesRoutes = new Hono();
7064
+ const forwardedHeaders = [
7065
+ "content-type",
7066
+ "retry-after",
7067
+ "openai-model",
7068
+ "x-request-id",
7069
+ "x-github-request-id",
7070
+ "x-copilot-service-request-id"
7071
+ ];
7072
+ const copyForwardedHeaders = (source) => {
7073
+ const headers = new Headers();
7074
+ for (const name of forwardedHeaders) {
7075
+ const value = source.get(name);
7076
+ if (value !== null) headers.set(name, value);
7077
+ }
7078
+ return headers;
7079
+ };
7080
+ const readSubagent = (value) => {
7081
+ if (value === void 0) return;
7082
+ return /^[\w.-]{1,64}$/.test(value) ? value : "other";
7083
+ };
7084
+ /**
7085
+ * Passes the upstream body to the client and settles the request trace when it ends.
7086
+ *
7087
+ * The trace cancels every upstream body still open once the handler settles, so a streamed answer
7088
+ * must settle the handler only after its last byte, an error, or the client's cancellation.
7089
+ */
7090
+ const settleWhenDone = (body, trace) => {
7091
+ const reader = body.getReader();
7092
+ let settled = false;
7093
+ const settle = (error) => {
7094
+ if (settled) return;
7095
+ settled = true;
7096
+ trace?.handlerSettled(error);
7097
+ };
7098
+ return new ReadableStream({
7099
+ pull: async (controller) => {
7100
+ try {
7101
+ const next = await reader.read();
7102
+ if (settled) return;
7103
+ if (next.done) {
7104
+ controller.close();
7105
+ settle();
7106
+ return;
7107
+ }
7108
+ controller.enqueue(next.value);
7109
+ } catch (error) {
7110
+ if (!settled) controller.error(error);
7111
+ settle(error);
7112
+ }
7113
+ },
7114
+ cancel: async (reason) => {
7115
+ settle();
7116
+ await reader.cancel(reason).catch(() => {});
7117
+ }
7118
+ }, { highWaterMark: 0 });
7119
+ };
7120
+ responsesRoutes.post("/responses", async (c) => {
7121
+ const config = c.get("config");
7122
+ const requestId = c.get("requestId");
7123
+ const trace = c.get("requestTrace");
7124
+ const signal = createCopilotRequestSignal(c.req.raw.signal, config.upstreamTimeoutMs);
7125
+ let response;
7126
+ let stream = false;
7127
+ try {
7128
+ const payload = parseCodexPayload(await c.req.text());
7129
+ stream = payload.stream === true;
7130
+ const model = await resolveCodexModel(config, payload.model);
7131
+ const prepared = prepareCodexPayload(payload, model, getCachedCopilotModel(config, model));
7132
+ const subagent = readSubagent(c.req.header("x-openai-subagent"));
7133
+ const initiator = getCodexInitiator(payload.input, subagent);
7134
+ log.info(sanitizeTerminalString([
7135
+ `request_id=${requestId}`,
7136
+ "Model request client=codex",
7137
+ `requested_model=${payload.model}`,
7138
+ `upstream_model=${model}`,
7139
+ "upstream_api=responses",
7140
+ `requested_think_effort=${prepared.requestedEffort ?? "unset"}`,
7141
+ `effective_think_effort=${prepared.effectiveEffort ?? "omitted"}`,
7142
+ `initiator=${initiator}`,
7143
+ ...subagent === void 0 ? [] : [`subagent=${subagent}`],
7144
+ ...prepared.removed.length === 0 ? [] : [`removed=${prepared.removed.join(",")}`]
7145
+ ].join(" ")));
7146
+ response = await fetchCopilot(getCopilotProviderContext(config), "/responses", {
7147
+ method: "POST",
7148
+ headers: {
7149
+ accept: stream ? "text/event-stream" : "application/json",
7150
+ "content-type": "application/json"
7151
+ },
7152
+ body: JSON.stringify(prepared.payload)
7153
+ }, {
7154
+ initiator,
7155
+ requestId,
7156
+ signal,
7157
+ timeoutMs: config.upstreamTimeoutMs,
7158
+ vision: includesImage(prepared.payload.input)
7159
+ });
7160
+ if (!response.ok) {
7161
+ const detail = await readCopilotText(response, signal, config.upstreamTimeoutMs);
7162
+ const overflow = toPromptTooLongError(response.status, detail);
7163
+ if (overflow && stream) {
7164
+ c.set("requestErrorMessage", overflow.message);
7165
+ log.error(`request_id=${requestId} ${overflow.message}; sent to Codex as context_length_exceeded`);
7166
+ return new Response(contextOverflowStream(requestId, overflow.message), {
7167
+ status: 200,
7168
+ headers: {
7169
+ "content-type": "text/event-stream",
7170
+ "cache-control": "no-cache"
7171
+ }
7172
+ });
7173
+ }
7174
+ const message = `Copilot /responses returned ${response.status}`;
7175
+ c.set("requestErrorMessage", message);
7176
+ log.error(`request_id=${requestId} ${message}`);
7177
+ return new Response(detail, {
7178
+ status: response.status,
7179
+ headers: copyForwardedHeaders(response.headers)
7180
+ });
7181
+ }
7182
+ } catch (error) {
7183
+ if (error instanceof ProxyNotImplementedError) {
7184
+ c.set("requestErrorMessage", error.message);
7185
+ return c.json({ error: {
7186
+ message: error.message,
7187
+ type: "server_error",
7188
+ code: "relay_not_configured"
7189
+ } }, 501);
7190
+ }
7191
+ if (!(error instanceof HTTPError)) throw error;
7192
+ const message = sanitizeTerminalString(error.message);
7193
+ c.set("requestErrorMessage", message);
7194
+ log.error(`request_id=${requestId} ${message}`);
7195
+ if (error.response.status === 400 && trace?.manifest.exchanges.length === 0) trace.recordFailure("local-validation");
7196
+ return new Response(await error.response.arrayBuffer(), {
7197
+ status: error.response.status,
7198
+ headers: copyForwardedHeaders(error.response.headers)
7199
+ });
7200
+ }
7201
+ const headers = copyForwardedHeaders(response.headers);
7202
+ if (stream) headers.set("cache-control", "no-cache");
7203
+ if (!response.body) return new Response(null, {
7204
+ status: response.status,
7205
+ headers
7206
+ });
7207
+ trace?.deferHandler();
7208
+ return new Response(settleWhenDone(response.body, trace), {
7209
+ status: response.status,
7210
+ headers
7211
+ });
7212
+ });
7213
+
6900
7214
  //#endregion
6901
7215
  //#region src/server.ts
6902
7216
  const loggedRequestHeaders = [
@@ -6951,6 +7265,11 @@ const presentsApiKey = (headers, apiKey) => {
6951
7265
  for (const presented of [headers.get("x-api-key"), bearer]) if (typeof presented === "string" && timingSafeEqual(sha256(presented), expected)) matched = true;
6952
7266
  return matched;
6953
7267
  };
7268
+ const jsonBodyPaths = new Set([
7269
+ "/v1/messages",
7270
+ "/v1/messages/count_tokens",
7271
+ "/v1/responses"
7272
+ ]);
6954
7273
  const isOpenProbe = (method, path$1) => method === "GET" && path$1 === "/healthz" || (method === "GET" || method === "HEAD") && path$1 === "/api/hello";
6955
7274
  const createServer = (config) => {
6956
7275
  const app = new Hono();
@@ -7007,7 +7326,7 @@ const createServer = (config) => {
7007
7326
  }
7008
7327
  }, 401, { "www-authenticate": "Bearer" });
7009
7328
  }
7010
- if (c.req.method === "POST" && (c.req.path === "/v1/messages" || c.req.path === "/v1/messages/count_tokens") && (incoming ? Boolean(incoming.headers["transfer-encoding"]) || Number(incoming.headers["content-length"] ?? 0) > 0 : c.req.raw.body !== null) && c.req.header("content-type")?.split(";", 1)[0].trim().toLowerCase() !== "application/json") {
7329
+ if (c.req.method === "POST" && jsonBodyPaths.has(c.req.path) && (incoming ? Boolean(incoming.headers["transfer-encoding"]) || Number(incoming.headers["content-length"] ?? 0) > 0 : c.req.raw.body !== null) && c.req.header("content-type")?.split(";", 1)[0].trim().toLowerCase() !== "application/json") {
7011
7330
  const message = "Content-Type must be application/json";
7012
7331
  c.set("requestErrorMessage", message);
7013
7332
  return c.json({ error: { message } }, 415);
@@ -7051,6 +7370,7 @@ const createServer = (config) => {
7051
7370
  }));
7052
7371
  app.on(["GET", "HEAD"], "/api/hello", (c) => c.body(null, 200));
7053
7372
  app.route("/v1", claudeRoutes);
7373
+ app.route("/v1", responsesRoutes);
7054
7374
  app.notFound(async (c) => {
7055
7375
  const message = "Unsupported Claude API route";
7056
7376
  c.set("requestErrorMessage", message);
@@ -9514,7 +9834,7 @@ const usage = defineCommand({
9514
9834
  await runMain(defineCommand({
9515
9835
  meta: {
9516
9836
  name: "copilot-relay",
9517
- description: "Yet, just another relay for Claude Code to use a GitHub Copilot subscription."
9837
+ description: "Yet, just another relay for Claude Code and Codex to use a GitHub Copilot subscription."
9518
9838
  },
9519
9839
  subCommands: {
9520
9840
  auth,