@jeffreycao/copilot-api 2.2.11 → 2.2.13

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
@@ -264,6 +264,42 @@ enabled = false
264
264
  >
265
265
  > For third-party models that do not support `tool_search`, we recommend disabling features.apps. Otherwise, each prompt may consume an additional 20,000 or more tokens.
266
266
 
267
+ ### If Codex Is Not Signed In to a GPT Account
268
+
269
+ ```toml
270
+ [model_providers.copilot_api]
271
+ name = "OpenAI"
272
+ base_url = "http://localhost:4141"
273
+ requires_openai_auth = false
274
+ supports_websockets = false
275
+ wire_api = "responses"
276
+ request_max_retries = 3
277
+ stream_max_retries = 3
278
+ stream_idle_timeout_ms = 300000
279
+
280
+ [model_providers.copilot_api.auth]
281
+ command = "powershell.exe"
282
+ args = [
283
+ "-NoProfile",
284
+ "-NonInteractive",
285
+ "-Command",
286
+ "[Console]::Out.Write($env:GITHUB_COPILOT_API_KEY)"
287
+ ]
288
+ ```
289
+
290
+ macOS, replace the `auth` block with:
291
+
292
+ ```toml
293
+ [model_providers.copilot_api.auth]
294
+ command = "/bin/zsh"
295
+ args = [
296
+ "-c",
297
+ "printf '%s' \"$GITHUB_COPILOT_API_KEY\""
298
+ ]
299
+ ```
300
+
301
+ Without this configuration, Codex cannot fetch `/v1/models` while not signed in to a GPT account, so custom models are unavailable in the model picker.
302
+
267
303
  When a Codex client (`User-Agent` starts with `codex`) requests the top-level `GET /v1/models`, the gateway merges native Codex models with models available through the Messages adapter. The latter advertise `use_responses_lite: true`: `/v1/responses` uses **Responses → Messages** for Anthropic providers, while OpenAI-compatible providers and Chat-only Copilot models reuse the existing Messages route for **Responses → Messages → Chat Completions**, then translate streaming or JSON results back to Responses.
268
304
 
269
305
  The merged catalog is what Codex shows in its model picker, including the models exposed by your configured providers:
package/README.zh-CN.md CHANGED
@@ -280,6 +280,42 @@ enabled = false
280
280
  >
281
281
  > 对于不支持 `tool_search` 的第三方模型,我们建议禁用 features.apps。否则,每个提示可能会额外消耗 20,000 多个 token。
282
282
 
283
+ ### Codex 未登录 GPT 账号时
284
+
285
+ ```toml
286
+ [model_providers.copilot_api]
287
+ name = "OpenAI"
288
+ base_url = "http://localhost:4141"
289
+ requires_openai_auth = false
290
+ supports_websockets = false
291
+ wire_api = "responses"
292
+ request_max_retries = 3
293
+ stream_max_retries = 3
294
+ stream_idle_timeout_ms = 300000
295
+
296
+ [model_providers.copilot_api.auth]
297
+ command = "powershell.exe"
298
+ args = [
299
+ "-NoProfile",
300
+ "-NonInteractive",
301
+ "-Command",
302
+ "[Console]::Out.Write($env:GITHUB_COPILOT_API_KEY)"
303
+ ]
304
+ ```
305
+
306
+ macOS 将 `auth` 段替换为:
307
+
308
+ ```toml
309
+ [model_providers.copilot_api.auth]
310
+ command = "/bin/zsh"
311
+ args = [
312
+ "-c",
313
+ "printf '%s' \"$GITHUB_COPILOT_API_KEY\""
314
+ ]
315
+ ```
316
+
317
+ 未按上述方式配置时,Codex 未登录 GPT 账号拉不到 `/v1/models`,无法选择自定义模型。
318
+
283
319
  Codex 客户端(`User-Agent` 以 `codex` 开头)请求顶层 `GET /v1/models` 时,网关会把原生 Codex 模型与可通过 Messages 适配的模型合并返回。后者会声明 `use_responses_lite: true`:调用 `/v1/responses` 后,Anthropic provider 走 **Responses → Messages**,OpenAI 兼容 provider 以及只支持 Chat 的 Copilot 模型则复用现有 Messages 路由继续走 **Responses → Messages → Chat Completions**,最终统一翻译回 Responses(包括流式事件)。
284
320
 
285
321
  合并后的模型列表会直接展示在 Codex 的模型选择界面中,包含各 provider 暴露的模型:
package/dist/main.js CHANGED
@@ -30,7 +30,7 @@ if (isMcpFastPath(process.argv)) {
30
30
  const { auth } = await import("./auth-C-RWNXGU.js");
31
31
  const { debug } = await import("./debug-41w6Whae.js");
32
32
  const { mcp } = await import("./mcp-fpSlKZxK.js");
33
- const { start } = await import("./start-DmLlG7PU.js");
33
+ const { start } = await import("./start-z_xmbJbc.js");
34
34
  await runMain(defineCommand({
35
35
  meta: {
36
36
  name: "copilot-api",
@@ -2933,8 +2933,8 @@ const setContextCacheControl = (part) => {
2933
2933
  part.cache_control = { ...OPENAI_COMPATIBLE_CONTEXT_CACHE_CONTROL };
2934
2934
  };
2935
2935
  //#endregion
2936
- //#region src/routes/provider/utils.ts
2937
- const RESPONSES_REASONING_EFFORTS = new Set([
2936
+ //#region src/lib/reasoning-effort.ts
2937
+ const REASONING_EFFORT_LEVELS = [
2938
2938
  "none",
2939
2939
  "minimal",
2940
2940
  "low",
@@ -2942,13 +2942,45 @@ const RESPONSES_REASONING_EFFORTS = new Set([
2942
2942
  "high",
2943
2943
  "xhigh",
2944
2944
  "max"
2945
- ]);
2945
+ ];
2946
+ const KNOWN_REASONING_EFFORTS = new Set(REASONING_EFFORT_LEVELS);
2946
2947
  const REASONING_EFFORT_ALIASES = {
2947
2948
  minimal: "low",
2948
- xhigh: "high",
2949
- max: "high",
2950
- ultra: "high"
2949
+ ultra: "max"
2950
+ };
2951
+ const reasoningEffortRank = (effort) => REASONING_EFFORT_LEVELS.indexOf(effort);
2952
+ /**
2953
+ * Resolves the reasoning effort to send upstream for a requested effort.
2954
+ *
2955
+ * Policy:
2956
+ * - A supported effort passes through unchanged.
2957
+ * - Client-only levels are aliased first ("ultra" -> "max").
2958
+ * - An unsupported request clamps to the highest supported level not above
2959
+ * the request, e.g. "max" against ["low", "medium", "high", "xhigh"]
2960
+ * resolves to "xhigh".
2961
+ * - A request below every supported level resolves to the lowest supported
2962
+ * level, e.g. "none" against ["low", "medium", "high"] resolves to "low".
2963
+ * - Without any known supported levels, valid wire efforts pass through and
2964
+ * client-facing aliases are converted (for example, "ultra" -> "max").
2965
+ * - An unrecognized request is left unresolved so the upstream can validate
2966
+ * it instead of receiving a silently substituted effort.
2967
+ */
2968
+ const resolveSupportedReasoningEffort = (requestedEffort, supportedEfforts) => {
2969
+ const supported = (supportedEfforts ?? []).filter((effort) => KNOWN_REASONING_EFFORTS.has(effort));
2970
+ const requestedWireEffort = KNOWN_REASONING_EFFORTS.has(requestedEffort) ? requestedEffort : void 0;
2971
+ if (supported.length === 0) return requestedWireEffort ?? REASONING_EFFORT_ALIASES[requestedEffort];
2972
+ if (requestedWireEffort && supported.includes(requestedWireEffort)) return requestedWireEffort;
2973
+ const aliasedEffort = REASONING_EFFORT_ALIASES[requestedEffort] ?? requestedWireEffort;
2974
+ if (!aliasedEffort) return;
2975
+ if (supported.includes(aliasedEffort)) return aliasedEffort;
2976
+ const requestedRank = reasoningEffortRank(aliasedEffort);
2977
+ let nearestBelow;
2978
+ for (const effort of supported) if (reasoningEffortRank(effort) <= requestedRank && (nearestBelow === void 0 || reasoningEffortRank(effort) > reasoningEffortRank(nearestBelow))) nearestBelow = effort;
2979
+ if (nearestBelow) return nearestBelow;
2980
+ return supported.reduce((lowest, effort) => reasoningEffortRank(effort) < reasoningEffortRank(lowest) ? effort : lowest);
2951
2981
  };
2982
+ //#endregion
2983
+ //#region src/routes/provider/utils.ts
2952
2984
  const applyModelDefaults = (payload, modelConfig) => {
2953
2985
  payload.temperature ??= modelConfig?.temperature;
2954
2986
  payload.top_p ??= modelConfig?.topP;
@@ -2958,22 +2990,19 @@ const applyMissingExtraBody = (payload, options) => {
2958
2990
  for (const [key, value] of Object.entries(options.extraBody ?? {})) if (!Object.hasOwn(payload, key)) payload[key] = value;
2959
2991
  };
2960
2992
  const normalizeProviderResponsesReasoningEffort = (payload, providerConfig) => {
2961
- const currentEffort = payload.reasoning?.effort;
2962
- if (typeof currentEffort !== "string") return;
2993
+ if (!payload.reasoning || typeof payload.reasoning.effort !== "string") return;
2963
2994
  const modelConfig = providerConfig.models?.[payload.model];
2964
2995
  const builtinModelConfig = builtinProviderModelRegistry.getModelConfig(providerConfig.name, payload.model);
2965
- const configuredEfforts = normalizeResponsesReasoningEfforts(modelConfig?.reasoningEfforts);
2966
- const builtinEfforts = normalizeResponsesReasoningEfforts(builtinModelConfig?.reasoningEfforts);
2967
- const supportedEfforts = configuredEfforts.length > 0 ? configuredEfforts : builtinEfforts;
2968
- if (supportedEfforts.length === 0 || supportedEfforts.includes(currentEffort)) return;
2969
- const aliasedEffort = REASONING_EFFORT_ALIASES[currentEffort];
2970
- const fallbackEffort = aliasedEffort && supportedEfforts.includes(aliasedEffort) ? aliasedEffort : selectSupportedReasoningEffort(supportedEfforts, modelConfig?.defaultReasoningEffort, builtinModelConfig?.defaultReasoningEffort);
2971
- if (payload.reasoning && fallbackEffort) payload.reasoning.effort = fallbackEffort;
2972
- };
2973
- const normalizeResponsesReasoningEfforts = (efforts) => (efforts ?? []).filter((effort) => RESPONSES_REASONING_EFFORTS.has(effort));
2974
- const selectSupportedReasoningEffort = (supportedEfforts, ...defaults) => {
2975
- for (const defaultEffort of defaults) if (defaultEffort && supportedEfforts.includes(defaultEffort)) return defaultEffort;
2976
- return supportedEfforts[0];
2996
+ const configuredEfforts = modelConfig?.reasoningEfforts;
2997
+ const supportedEfforts = configuredEfforts && configuredEfforts.length > 0 ? configuredEfforts : builtinModelConfig?.reasoningEfforts;
2998
+ const resolvedEffort = resolveSupportedReasoningEffort(payload.reasoning.effort, supportedEfforts);
2999
+ if (!resolvedEffort || resolvedEffort === payload.reasoning.effort) return;
3000
+ const requestedEffort = payload.reasoning.effort;
3001
+ payload.reasoning.effort = resolvedEffort;
3002
+ return {
3003
+ from: requestedEffort,
3004
+ to: resolvedEffort
3005
+ };
2977
3006
  };
2978
3007
  //#endregion
2979
3008
  //#region src/routes/provider/chat-completions/handler.ts
@@ -5910,6 +5939,17 @@ const getStreamErrorMessage = (event) => {
5910
5939
  return event.error?.message ?? event.message ?? void 0;
5911
5940
  };
5912
5941
  const DEFAULT_RESPONSES_COMPACT_THRESHOLD_RATIO = .85;
5942
+ const normalizeResponsesReasoningEffort = (payload, supportedEfforts) => {
5943
+ if (!payload.reasoning || typeof payload.reasoning.effort !== "string") return;
5944
+ const resolvedEffort = resolveSupportedReasoningEffort(payload.reasoning.effort, supportedEfforts);
5945
+ if (!resolvedEffort || resolvedEffort === payload.reasoning.effort) return;
5946
+ const requestedEffort = payload.reasoning.effort;
5947
+ payload.reasoning.effort = resolvedEffort;
5948
+ return {
5949
+ from: requestedEffort,
5950
+ to: resolvedEffort
5951
+ };
5952
+ };
5913
5953
  const responsesUtilsDependencies = {
5914
5954
  getModelResponsesApiCompactThreshold: getModelResponsesApiCompactThreshold$1,
5915
5955
  isContextManagementEnabledForMessages,
@@ -6651,7 +6691,8 @@ async function handleProviderMessagesForProvider(c, options) {
6651
6691
  const handleOpenAIResponsesProviderWebSearchMessages = async (c, options) => {
6652
6692
  const { modelConfig, payload, provider, providerConfig, usageEndpoint } = options;
6653
6693
  const responsesPayload = prepareWebSearchResponsesPayload(payload);
6654
- normalizeProviderResponsesReasoningEffort(responsesPayload, providerConfig);
6694
+ const normalizedReasoningEffort = normalizeProviderResponsesReasoningEffort(responsesPayload, providerConfig);
6695
+ if (normalizedReasoningEffort) logger$10.debug(`Normalized reasoning effort from ${normalizedReasoningEffort.from} to ${normalizedReasoningEffort.to} based on the provider model configuration`);
6655
6696
  debugJson(logger$10, "provider.messages.responses.web_search.request", {
6656
6697
  payload: responsesPayload,
6657
6698
  provider
@@ -6715,7 +6756,8 @@ const handleOpenAIResponsesProviderMessages = async (c, options) => {
6715
6756
  const selectedModel = providerConfig.name === "codex" ? getModels().data.find((model) => model.id === payload.model) : void 0;
6716
6757
  const wantsStream = payload.stream === true;
6717
6758
  const responsesPayload = translateAnthropicMessagesToResponsesPayload(payload);
6718
- normalizeProviderResponsesReasoningEffort(responsesPayload, providerConfig);
6759
+ const normalizedMessagesReasoningEffort = normalizeProviderResponsesReasoningEffort(responsesPayload, providerConfig);
6760
+ if (normalizedMessagesReasoningEffort) logger$10.debug(`Normalized reasoning effort from ${normalizedMessagesReasoningEffort.from} to ${normalizedMessagesReasoningEffort.to} based on the provider model configuration`);
6719
6761
  if (providerConfig.name === "codex" && !wantsStream) responsesPayload.stream = true;
6720
6762
  if (applyResponsesApiContextManagement(responsesPayload, selectedModel?.capabilities.limits.max_prompt_tokens, { source: "messages" })) compactInputByLatestCompaction(responsesPayload);
6721
6763
  debugJson(logger$10, "provider.messages.responses.request", {
@@ -7641,6 +7683,10 @@ const DEFAULT_REASONING_EFFORTS = [
7641
7683
  "max",
7642
7684
  "ultra"
7643
7685
  ];
7686
+ const addUltraReasoningEffort = (efforts) => {
7687
+ if (efforts.includes("ultra")) return efforts;
7688
+ return [...efforts, "ultra"];
7689
+ };
7644
7690
  const CODEX_ALIAS_PRIORITY_BASE = 1e3;
7645
7691
  const COPILOT_PRIORITY_BASE = 2e3;
7646
7692
  const OPENCODE_GO_PRIORITY_BASE = 3e3;
@@ -7947,7 +7993,7 @@ function createCatalogAlias(model, slug, providerName) {
7947
7993
  };
7948
7994
  }
7949
7995
  function createSyntheticCodexModel(candidate, template, priority) {
7950
- const reasoningEfforts = candidate.reasoningEfforts.length > 0 ? candidate.reasoningEfforts : DEFAULT_REASONING_EFFORTS;
7996
+ const reasoningEfforts = candidate.reasoningEfforts.length > 0 ? addUltraReasoningEffort(candidate.reasoningEfforts) : DEFAULT_REASONING_EFFORTS;
7951
7997
  const defaultReasoningEffort = reasoningEfforts.includes(candidate.defaultReasoningEffort) ? candidate.defaultReasoningEffort : reasoningEfforts[0];
7952
7998
  const supportsReasoning = reasoningEfforts.some((effort) => effort !== "none");
7953
7999
  const inputModalities = [...new Set(candidate.inputModalities)];
@@ -8697,17 +8743,11 @@ function registerMessagesTool(registration, registry) {
8697
8743
  const originalKey = createOriginalToolKey(registration);
8698
8744
  const existing = registry.byOriginal.get(originalKey);
8699
8745
  if (existing) return existing;
8700
- const alias = createToolAlias(registration.namespace ? `${registration.namespace.replaceAll(".", "_")}__${registration.name}` : registration.name, originalKey, registry);
8701
- const descriptor = {
8702
- alias,
8703
- kind: registration.kind,
8704
- name: registration.name,
8705
- ...registration.namespace ? { namespace: registration.namespace } : {}
8706
- };
8707
- registry.byAlias.set(alias, descriptor);
8746
+ const descriptor = createMessagesToolDescriptor(registration, registry);
8747
+ registry.byAlias.set(descriptor.alias, descriptor);
8708
8748
  registry.byOriginal.set(originalKey, descriptor);
8709
8749
  registry.tools.push({
8710
- name: alias,
8750
+ name: descriptor.alias,
8711
8751
  ...registration.description ? { description: registration.description } : {},
8712
8752
  input_schema: registration.kind === "custom" ? CUSTOM_TOOL_INPUT_SCHEMA : registration.parameters ?? {
8713
8753
  type: "object",
@@ -8717,6 +8757,15 @@ function registerMessagesTool(registration, registry) {
8717
8757
  });
8718
8758
  return descriptor;
8719
8759
  }
8760
+ function createMessagesToolDescriptor(registration, registry) {
8761
+ const originalKey = createOriginalToolKey(registration);
8762
+ return {
8763
+ alias: createToolAlias(registration.namespace ? `${registration.namespace.replaceAll(".", "_")}__${registration.name}` : registration.name, originalKey, registry),
8764
+ kind: registration.kind,
8765
+ name: registration.name,
8766
+ ...registration.namespace ? { namespace: registration.namespace } : {}
8767
+ };
8768
+ }
8720
8769
  function translateInputToAnthropic(input, registry, instructions, originalInput, toolCallTips) {
8721
8770
  const messages = [];
8722
8771
  const system = [];
@@ -8847,11 +8896,12 @@ function translateInputReasoning(item, messages) {
8847
8896
  }
8848
8897
  function translateInputToolCall(item, kind, messages, registry) {
8849
8898
  if (!isRecord(item)) throw new ResponsesMessagesTranslationError("Invalid Responses tool call item");
8850
- const descriptor = registerMessagesTool({
8899
+ const toolIdentity = {
8851
8900
  kind,
8852
8901
  name: requireStringField(item, "name", `${kind}_tool_call`),
8853
8902
  namespace: getOptionalStringField(item, "namespace") ?? void 0
8854
- }, registry);
8903
+ };
8904
+ const descriptor = registry.byOriginal.get(createOriginalToolKey(toolIdentity)) ?? createMessagesToolDescriptor(toolIdentity, registry);
8855
8905
  const input = kind === "custom" ? { input: getStringField(item, "input") ?? "" } : parseFunctionArguments(getStringField(item, "arguments") ?? "{}", `${kind}_tool_call.arguments`);
8856
8906
  appendAssistantBlock(messages, {
8857
8907
  type: "tool_use",
@@ -10065,7 +10115,8 @@ async function handleProviderResponsesForProvider(c, options) {
10065
10115
  type: "invalid_request_error"
10066
10116
  } }, 400);
10067
10117
  const effectiveType = resolveEffectiveProviderType(providerConfig, payload.model);
10068
- normalizeProviderResponsesReasoningEffort(payload, providerConfig);
10118
+ const normalizedReasoningEffort = normalizeProviderResponsesReasoningEffort(payload, providerConfig);
10119
+ if (normalizedReasoningEffort) logger$2.debug(`Normalized reasoning effort from ${normalizedReasoningEffort.from} to ${normalizedReasoningEffort.to} based on the provider model configuration`);
10069
10120
  if (shouldFallbackToMessages$1(c, payload.model, effectiveType)) return await handleResponsesViaMessages(c, {
10070
10121
  payload,
10071
10122
  publicModel: options.publicModel ?? payload.model,
@@ -10284,6 +10335,8 @@ const handleResponses = async (c) => {
10284
10335
  logger$1.debug("Extracted session ID:", fallbackSessionId);
10285
10336
  const selectedModel = responsesHandlerDependencies.findEndpointModel(payload.model);
10286
10337
  payload.model = selectedModel?.id ?? payload.model;
10338
+ const normalizedReasoningEffort = normalizeResponsesReasoningEffort(payload, selectedModel?.capabilities?.supports?.reasoning_effort);
10339
+ if (normalizedReasoningEffort) logger$1.debug(`Normalized reasoning effort from ${normalizedReasoningEffort.from} to ${normalizedReasoningEffort.to} based on the selected model capabilities`);
10287
10340
  const responsesTransport = getResponsesTransportForModel(selectedModel);
10288
10341
  if (shouldFallbackToMessages(c, payload.model, selectedModel, responsesTransport)) return await handleResponsesViaMessages(c, {
10289
10342
  payload,
@@ -10569,4 +10622,4 @@ server.route("/:provider/images", providerImageRoutes);
10569
10622
  //#endregion
10570
10623
  export { server };
10571
10624
 
10572
- //# sourceMappingURL=server-D_qrybxD.js.map
10625
+ //# sourceMappingURL=server-Bmd_z-tv.js.map