@bitkyc08/opencodex 2.19.0 → 2.21.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.
Files changed (130) hide show
  1. package/gui/dist/assets/index-BOFeam5a.js +76 -0
  2. package/gui/dist/assets/index-Xq49CY8F.css +1 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +1 -1
  5. package/src/adapters/anthropic.ts +2 -28
  6. package/src/adapters/google.ts +31 -3
  7. package/src/adapters/mimo-free.ts +17 -0
  8. package/src/adapters/openai-chat.ts +392 -40
  9. package/src/adapters/registry.ts +144 -0
  10. package/src/adapters/responses-tool-schema.ts +67 -0
  11. package/src/bridge.ts +15 -2
  12. package/src/chat/inbound.ts +13 -7
  13. package/src/claude/gateway-cache.ts +41 -4
  14. package/src/cli/claude.ts +3 -2
  15. package/src/cli/combo.ts +3 -0
  16. package/src/cli/dispatch.ts +8 -0
  17. package/src/cli/export-command.ts +2 -2
  18. package/src/cli/help.ts +2 -0
  19. package/src/cli/index.ts +3 -2
  20. package/src/cli/lab.ts +135 -1
  21. package/src/cli/minimax.ts +491 -0
  22. package/src/cli/models-runtime.ts +22 -1
  23. package/src/cli/models.ts +67 -2
  24. package/src/cli/opencode.ts +2 -1
  25. package/src/cli/registry.ts +22 -2
  26. package/src/clients/config-export.ts +125 -7
  27. package/src/codex/app-server-processes.ts +57 -2
  28. package/src/codex/app-server-restart-service.ts +232 -0
  29. package/src/codex/catalog/aggregation.ts +10 -1
  30. package/src/codex/catalog/effort.ts +15 -3
  31. package/src/codex/catalog/parsing.ts +3 -1
  32. package/src/codex/catalog/provider-fetch.ts +45 -5
  33. package/src/codex/catalog/sync.ts +74 -4
  34. package/src/codex/convergence.ts +2 -0
  35. package/src/combos/index.ts +1 -0
  36. package/src/combos/request.ts +30 -0
  37. package/src/combos/types.ts +6 -0
  38. package/src/config.ts +52 -0
  39. package/src/generated/compatibility-version.json +241 -85
  40. package/src/images/loop.ts +26 -6
  41. package/src/integrations/registry.ts +7 -0
  42. package/src/lab/conformance/jcs.ts +42 -2
  43. package/src/lab/conformance/negative-controls.ts +6 -2
  44. package/src/lab/conformance/runner.ts +16 -5
  45. package/src/lab/fabric/observe.ts +49 -14
  46. package/src/lab/index.ts +16 -0
  47. package/src/lab/ledger/purge.ts +152 -83
  48. package/src/lab/ledger/store.ts +168 -54
  49. package/src/lab/observe/from-conformance.ts +8 -6
  50. package/src/lab/observe/from-live.ts +8 -2
  51. package/src/lab/paths.ts +23 -0
  52. package/src/lab/public/bundle.ts +217 -0
  53. package/src/lab/public/community-authority.ts +175 -0
  54. package/src/lab/public/community-files.ts +29 -0
  55. package/src/lab/public/community.ts +479 -0
  56. package/src/lab/public/file-safety.ts +155 -0
  57. package/src/lab/public/ids.ts +26 -0
  58. package/src/lab/public/index.ts +16 -0
  59. package/src/lab/public/mutation-lock.ts +424 -0
  60. package/src/lab/public/operator.ts +353 -0
  61. package/src/lab/public/origin-purge.ts +79 -0
  62. package/src/lab/public/origin.ts +203 -0
  63. package/src/lab/public/privacy.ts +143 -0
  64. package/src/lab/public/private-file.ts +261 -0
  65. package/src/lab/public/project.ts +124 -0
  66. package/src/lab/public/purge-test-fault.ts +21 -0
  67. package/src/lab/public/purge.ts +223 -0
  68. package/src/lab/public/registry.ts +44 -0
  69. package/src/lab/public/revocation.ts +252 -0
  70. package/src/lab/public/signature.ts +219 -0
  71. package/src/lab/public/storage.ts +105 -0
  72. package/src/lab/public/strict-json.ts +206 -0
  73. package/src/lab/public/time.ts +26 -0
  74. package/src/lab/public/types.ts +172 -0
  75. package/src/lab/public/validate.ts +391 -0
  76. package/src/lib/codex-restart-contract.ts +120 -0
  77. package/src/lib/lab-activation.ts +109 -47
  78. package/src/lib/lab-live-pinned-sender.ts +16 -5
  79. package/src/lib/pinned-http.ts +70 -16
  80. package/src/lib/self-launch-argv.ts +15 -0
  81. package/src/lib/state-store-registrations.ts +2 -0
  82. package/src/lib/upstream-reachability.ts +4 -0
  83. package/src/lib/windows-elevation.ts +10 -1
  84. package/src/providers/derive.ts +24 -4
  85. package/src/providers/registry.ts +14 -4
  86. package/src/providers/request-pacing.ts +310 -0
  87. package/src/providers/service-tier.ts +143 -0
  88. package/src/providers/static-model-discovery.ts +86 -0
  89. package/src/reasoning-effort.ts +27 -1
  90. package/src/responses/custom-tool-compat.ts +4 -1
  91. package/src/responses/parser.ts +7 -1
  92. package/src/responses/provider-opaque-metadata.ts +73 -0
  93. package/src/responses/schema.ts +6 -0
  94. package/src/router.ts +23 -6
  95. package/src/routing/capability.ts +4 -2
  96. package/src/routing/compatibility/behavior.ts +5 -1
  97. package/src/server/adapter-resolve.ts +2 -32
  98. package/src/server/auth-cors.ts +50 -6
  99. package/src/server/chat-completions.ts +74 -36
  100. package/src/server/chat-native-sse.ts +331 -0
  101. package/src/server/chat-native.ts +371 -0
  102. package/src/server/management/combo-routes.ts +16 -2
  103. package/src/server/management/config-routes.ts +6 -4
  104. package/src/server/management/context.ts +17 -0
  105. package/src/server/management/lab-routes.ts +181 -19
  106. package/src/server/management/model-routes.ts +76 -2
  107. package/src/server/management/model-rows.ts +8 -0
  108. package/src/server/management/provider-capability-config.ts +48 -0
  109. package/src/server/management/provider-routes.ts +76 -4
  110. package/src/server/management/system-restart.ts +4 -2
  111. package/src/server/management/system-routes.ts +38 -0
  112. package/src/server/relay.ts +17 -3
  113. package/src/server/responses/compact.ts +4 -1
  114. package/src/server/responses/core.ts +257 -46
  115. package/src/server/responses/empty-completion-guard.ts +276 -0
  116. package/src/server/responses/fetch-helpers.ts +35 -4
  117. package/src/server/responses/pacing-overload.ts +13 -0
  118. package/src/server/responses/policy-fallback.ts +16 -2
  119. package/src/server/responses/terminal-guard.ts +1 -1
  120. package/src/server/responses/upstream-error.ts +5 -0
  121. package/src/server/responses.ts +17 -2
  122. package/src/server/system-env.ts +1 -1
  123. package/src/types.ts +84 -5
  124. package/src/update/index.ts +6 -5
  125. package/src/update/job.ts +5 -6
  126. package/src/update/notify.ts +5 -3
  127. package/src/usage/log.ts +11 -1
  128. package/src/web-search/loop.ts +21 -5
  129. package/gui/dist/assets/index-CQ7bIKee.css +0 -1
  130. package/gui/dist/assets/index-D_JUZLEC.js +0 -76
@@ -12,7 +12,9 @@ import { identifyRoutedModel } from "./identity";
12
12
  import { peekReasoningForCall } from "../responses/reasoning-replay-cache";
13
13
  import { buildNonOpenAIToolCatalogNudgeForTools, shouldInjectNonOpenAIToolCatalogNudge } from "./tool-catalog-nudge";
14
14
  import { openRouterProviderPayload, resolveOpenRouterRouting } from "../providers/openrouter-routing";
15
+ import { canSerializeServiceTierForChatModel } from "../providers/service-tier";
15
16
  import { openaiChatCompletionsUrl } from "./openai-chat-url";
17
+ import { stripResponsesOnlyEncryptedMarker } from "./responses-tool-schema";
16
18
  import {
17
19
  isTranslatorBudgetExceededError,
18
20
  retainTranslatedEventBatch,
@@ -34,6 +36,122 @@ export function stripBracketedModelSuffix(modelId: string): string {
34
36
  return suffixStart === -1 ? modelId : modelId.slice(0, suffixStart);
35
37
  }
36
38
 
39
+ const CHAT_PASSTHROUGH_FIELDS = [
40
+ "audio",
41
+ "frequency_penalty",
42
+ "logit_bias",
43
+ "logprobs",
44
+ "max_completion_tokens",
45
+ "max_tokens",
46
+ "metadata",
47
+ "modalities",
48
+ "n",
49
+ "prediction",
50
+ "presence_penalty",
51
+ "reasoning_effort",
52
+ "response_format",
53
+ "seed",
54
+ "stop",
55
+ "store",
56
+ "temperature",
57
+ "tool_choice",
58
+ "tools",
59
+ "top_logprobs",
60
+ "top_p",
61
+ "user",
62
+ "web_search_options",
63
+ ] as const;
64
+
65
+ function openAIChatTransport(provider: OcxProviderConfig): {
66
+ url: string;
67
+ headers: Record<string, string>;
68
+ hasCredential: boolean;
69
+ } {
70
+ const hasCredential = typeof provider.apiKey === "string" && provider.apiKey.trim().length > 0;
71
+ if ((provider.authMode === "key" || provider.authMode === "oauth") && !provider.keyOptional && !hasCredential) {
72
+ throw new Error(`${provider.adapter} requires a non-empty credential (authMode: ${provider.authMode})`);
73
+ }
74
+ const headers: Record<string, string> = { "Content-Type": "application/json" };
75
+ if (hasCredential) headers.Authorization = `Bearer ${provider.apiKey}`;
76
+ if (provider.headers) Object.assign(headers, provider.headers);
77
+ return { url: openaiChatCompletionsUrl(provider.baseUrl), headers, hasCredential };
78
+ }
79
+
80
+ /**
81
+ * Build a provider request from an inbound Chat Completions body without translating it
82
+ * through the Responses contract. This is deliberately a whitelist: Chat-only caller
83
+ * fields retain their exact wire representation, while provider capability gates remain
84
+ * centralized beside the ordinary openai-chat adapter.
85
+ */
86
+ export function buildOpenAIChatPassthroughRequest(
87
+ provider: OcxProviderConfig,
88
+ rawBody: Record<string, unknown>,
89
+ modelId: string,
90
+ stream: boolean,
91
+ ): AdapterRequest {
92
+ const { url, headers, hasCredential } = openAIChatTransport(provider);
93
+
94
+ const body: Record<string, unknown> = {
95
+ model: provider.modelSuffixBracketStrip ? stripBracketedModelSuffix(modelId) : modelId,
96
+ messages: rawBody.messages,
97
+ stream,
98
+ };
99
+ for (const field of CHAT_PASSTHROUGH_FIELDS) {
100
+ if (rawBody[field] !== undefined) body[field] = rawBody[field];
101
+ }
102
+
103
+ if (modelInList(provider.noTemperatureModels, modelId)) delete body.temperature;
104
+ if (modelInList(provider.noTopPModels, modelId)) delete body.top_p;
105
+ if (modelInList(provider.noPenaltyModels, modelId)) {
106
+ delete body.presence_penalty;
107
+ delete body.frequency_penalty;
108
+ }
109
+ if (modelInList(provider.noStructuredOutputModels, modelId)) delete body.response_format;
110
+
111
+ if (provider.chatServiceTier && rawBody.service_tier !== undefined) {
112
+ body.service_tier = rawBody.service_tier;
113
+ }
114
+ if (provider.promptCacheKey && rawBody.prompt_cache_key !== undefined) {
115
+ body.prompt_cache_key = rawBody.prompt_cache_key;
116
+ }
117
+ if (Array.isArray(rawBody.tools) && rawBody.tools.length > 0) {
118
+ if (provider.parallelToolCalls === true) {
119
+ body.parallel_tool_calls = rawBody.parallel_tool_calls !== false;
120
+ } else if (provider.parallelToolCalls === false
121
+ && (provider.baseUrl === "https://integrate.api.nvidia.com/v1" || provider.pinParallelToolCallsFalse === true)) {
122
+ body.parallel_tool_calls = false;
123
+ }
124
+ }
125
+ if (stream) {
126
+ const callerOptions = rawBody.stream_options !== null
127
+ && typeof rawBody.stream_options === "object"
128
+ && !Array.isArray(rawBody.stream_options)
129
+ ? rawBody.stream_options as Record<string, unknown>
130
+ : {};
131
+ body.stream_options = { ...callerOptions, include_usage: true };
132
+ } else if (rawBody.stream_options !== undefined) {
133
+ body.stream_options = rawBody.stream_options;
134
+ }
135
+
136
+ const bodyJson = JSON.stringify(body);
137
+
138
+ if (isDebugEnabled()) {
139
+ let host = "upstream";
140
+ try { host = new URL(url).host; } catch { /* keep fallback */ }
141
+ debugProviderDiagnostic("openai-chat", "passthrough-request", {
142
+ host,
143
+ model: body.model,
144
+ stream,
145
+ messageCount: Array.isArray(body.messages) ? body.messages.length : 0,
146
+ toolCount: Array.isArray(body.tools) ? body.tools.length : 0,
147
+ hasCredential,
148
+ bodyBytes: new TextEncoder().encode(bodyJson).length,
149
+ });
150
+ }
151
+
152
+ return { url, method: "POST", headers, body: bodyJson };
153
+ }
154
+
37
155
  // 260715 (issue #126): surface upstream error detail through the web-search sidecar loop.
38
156
  // loop.ts only appends a suffix to "Provider error N" when the adapter exposes
39
157
  // formatErrorBody; without it, strict OpenAI-compatible backends (NVIDIA NIM pydantic
@@ -215,6 +333,16 @@ type InvalidToolCallReason =
215
333
  | "tool_call_function_name_blank"
216
334
  | "tool_call_function_arguments_invalid";
217
335
 
336
+ /**
337
+ * Streamed string fields are absent when null or undefined (#1731): OpenAI-compatible
338
+ * streamers repeat already-sent `id`/`name`/`arguments` as null on continuation deltas.
339
+ * The accumulator and this diagnostic share this predicate so they cannot disagree about
340
+ * which delta was the invalid one.
341
+ */
342
+ function isInvalidStreamStringField(value: unknown): boolean {
343
+ return value != null && typeof value !== "string";
344
+ }
345
+
218
346
  /**
219
347
  * Explain only the rejected wire shape, never its values. This diagnostic exists so provider
220
348
  * compatibility can be tightened from evidence without retaining tool arguments or credentials.
@@ -241,6 +369,10 @@ function diagnoseInvalidToolCalls(
241
369
  // Blank names are caught later at flush, not here, so they are not diagnosed on this
242
370
  // branch. Describe exactly that boundary rather than tightening compatibility in a
243
371
  // diagnostic change.
372
+ // #1731: "present" means the same thing here as in the accumulator — null and undefined
373
+ // are both absent, because some OpenAI-compatible streamers repeat already-sent fields
374
+ // as null on continuation deltas. A separate predicate here would diagnose accepted
375
+ // padding as the failure and point compatibility work at the wrong delta.
244
376
  const streamFunction = (rawToolCall as { function?: unknown }).function;
245
377
  if (streamFunction !== undefined && streamFunction !== null) {
246
378
  if (!isRecord(streamFunction)) {
@@ -250,14 +382,14 @@ function diagnoseInvalidToolCalls(
250
382
  valueType: Array.isArray(streamFunction) ? "array" : typeof streamFunction,
251
383
  };
252
384
  }
253
- if (streamFunction.name !== undefined && typeof streamFunction.name !== "string") {
385
+ if (isInvalidStreamStringField(streamFunction.name)) {
254
386
  return { reason: "tool_call_function_name_invalid", callIndex, valueType: typeof streamFunction.name };
255
387
  }
256
- if (streamFunction.arguments !== undefined && typeof streamFunction.arguments !== "string") {
388
+ if (isInvalidStreamStringField(streamFunction.arguments)) {
257
389
  return { reason: "tool_call_function_arguments_invalid", callIndex, valueType: typeof streamFunction.arguments };
258
390
  }
259
391
  }
260
- if (rawToolCall.id !== undefined && typeof rawToolCall.id !== "string") {
392
+ if (isInvalidStreamStringField(rawToolCall.id)) {
261
393
  return { reason: "tool_call_id_invalid", callIndex, valueType: typeof rawToolCall.id };
262
394
  }
263
395
  continue;
@@ -658,11 +790,11 @@ function shouldSanitizeZenToolParameters(provider: OcxProviderConfig): boolean {
658
790
  || baseUrl === "https://opencode.ai/zen/go/v1";
659
791
  }
660
792
 
661
- const XAI_SCHEMA_BASE_URLS = new Set(["api.x.ai", "cli-chat-proxy.grok.com"]);
662
-
663
793
  function isXaiSchemaTarget(provider: OcxProviderConfig): boolean {
664
794
  try {
665
- return XAI_SCHEMA_BASE_URLS.has(new URL(provider.baseUrl).hostname);
795
+ // Public api.x.ai accepts native root object unions. Only the Grok CLI proxy
796
+ // 400s on a root oneOf/anyOf, so flattening/omitting is scoped to that host.
797
+ return new URL(provider.baseUrl).hostname === "cli-chat-proxy.grok.com";
666
798
  } catch {
667
799
  return false;
668
800
  }
@@ -696,36 +828,262 @@ function ensureRootObjectType(parameters: unknown): Record<string, unknown> {
696
828
  return { ...obj, type: "object" };
697
829
  }
698
830
 
831
+ function isXaiObjectSchema(value: unknown): value is Record<string, unknown> {
832
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
833
+ }
834
+
835
+ function stringRequiredFields(value: unknown): string[] {
836
+ return Array.isArray(value)
837
+ ? value.filter((item): item is string => typeof item === "string")
838
+ : [];
839
+ }
840
+
841
+ /** Variant keys the merger can keep. Anything else is refused, not silently dropped. */
842
+ const XAI_VARIANT_MERGE_KEYS = new Set([
843
+ "type",
844
+ "properties",
845
+ "required",
846
+ "additionalProperties",
847
+ "description",
848
+ "title",
849
+ "$comment",
850
+ "$defs",
851
+ "definitions",
852
+ ]);
853
+
854
+ function decodeJsonPointerToken(token: string): string {
855
+ return token.replace(/~1/g, "/").replace(/~0/g, "~");
856
+ }
857
+
858
+ function lookupLocalJsonPointer(root: unknown, ref: string): unknown {
859
+ if (ref === "#" || ref === "#/") return root;
860
+ if (!ref.startsWith("#/")) return undefined;
861
+ let current: unknown = root;
862
+ for (const token of ref.slice(2).split("/").map(decodeJsonPointerToken)) {
863
+ if (!isXaiObjectSchema(current) || !Object.hasOwn(current, token)) return undefined;
864
+ current = current[token];
865
+ }
866
+ return current;
867
+ }
868
+
869
+ /** Resolve local `#/` `$ref`s. Unresolvable or cyclic refs return undefined. */
870
+ function resolveXaiSchemaRefs(
871
+ schema: unknown,
872
+ root: Record<string, unknown>,
873
+ stack: Set<string> = new Set(),
874
+ ): unknown | undefined {
875
+ if (!isXaiObjectSchema(schema)) return schema;
876
+ if (typeof schema.$ref === "string") {
877
+ const ref = schema.$ref;
878
+ if (stack.has(ref)) return undefined;
879
+ const target = lookupLocalJsonPointer(root, ref);
880
+ if (target === undefined) return undefined;
881
+ stack.add(ref);
882
+ const resolvedTarget = resolveXaiSchemaRefs(target, root, stack);
883
+ stack.delete(ref);
884
+ if (resolvedTarget === undefined) return undefined;
885
+ const rest: Record<string, unknown> = { ...schema };
886
+ delete rest.$ref;
887
+ if (Object.keys(rest).length === 0) return resolvedTarget;
888
+ const resolvedRest = resolveXaiSchemaRefs(rest, root, stack);
889
+ if (resolvedRest === undefined || !isXaiObjectSchema(resolvedTarget) || !isXaiObjectSchema(resolvedRest)) {
890
+ return undefined;
891
+ }
892
+ return composeXaiObjectSchemas(resolvedTarget, resolvedRest);
893
+ }
894
+
895
+ const resolved: Record<string, unknown> = {};
896
+ for (const [key, value] of Object.entries(schema)) {
897
+ if ((key === "oneOf" || key === "anyOf") && Array.isArray(value)) {
898
+ const items: unknown[] = [];
899
+ for (const item of value) {
900
+ const next = resolveXaiSchemaRefs(item, root, stack);
901
+ if (next === undefined) return undefined;
902
+ items.push(next);
903
+ }
904
+ resolved[key] = items;
905
+ continue;
906
+ }
907
+ if (key === "properties" && isXaiObjectSchema(value)) {
908
+ const properties: Record<string, unknown> = {};
909
+ for (const [name, property] of Object.entries(value)) {
910
+ const next = resolveXaiSchemaRefs(property, root, stack);
911
+ if (next === undefined) return undefined;
912
+ properties[name] = next;
913
+ }
914
+ resolved[key] = properties;
915
+ continue;
916
+ }
917
+ resolved[key] = value;
918
+ }
919
+ return resolved;
920
+ }
921
+
922
+ function xaiVariantIsConcreteObject(variant: Record<string, unknown>): boolean {
923
+ if (variant.type !== undefined && variant.type !== "object") return false;
924
+ return Object.keys(variant).every(key => XAI_VARIANT_MERGE_KEYS.has(key));
925
+ }
926
+
927
+ function variantProperties(variant: Record<string, unknown>): Record<string, unknown> {
928
+ return isXaiObjectSchema(variant.properties) ? variant.properties : {};
929
+ }
930
+
931
+ /**
932
+ * Independent per-property anyOf is lossless only when every property name exists
933
+ * on every variant (absence is meaningful under xAI's default additionalProperties:
934
+ * false, and promoting a branch-local key also tightens explicit-true variants)
935
+ * and at most one of those shared properties has a conflicting schema.
936
+ */
937
+ function xaiPropertyMergeIsLossless(variants: Record<string, unknown>[]): boolean {
938
+ const names = new Set<string>();
939
+ const props = variants.map(variant => {
940
+ const properties = variantProperties(variant);
941
+ for (const name of Object.keys(properties)) names.add(name);
942
+ return properties;
943
+ });
944
+ let schemaConflicts = 0;
945
+ for (const name of names) {
946
+ const values = props.map(property => property[name]);
947
+ if (values.some(value => value === undefined)) return false;
948
+ if (values.some(value => JSON.stringify(value) !== JSON.stringify(values[0]))) schemaConflicts += 1;
949
+ }
950
+ return schemaConflicts <= 1;
951
+ }
952
+
953
+ function xaiRequiredSetsMatch(variants: Record<string, unknown>[]): boolean {
954
+ const serialized = variants.map(variant => [...stringRequiredFields(variant.required)].sort().join("\0"));
955
+ return serialized.every(value => value === serialized[0]);
956
+ }
957
+
958
+ function mergeXaiAdditionalProperties(
959
+ variants: Record<string, unknown>[],
960
+ ): { ok: true; value?: unknown } | { ok: false } {
961
+ const values = variants.map(variant => variant.additionalProperties);
962
+ const explicit = values.filter(value => value !== undefined);
963
+ if (explicit.length === 0) return { ok: true };
964
+ if (explicit.length !== values.length) return { ok: false };
965
+ const hasFalse = explicit.some(value => value === false);
966
+ const permissive = explicit.filter(value => value !== false);
967
+ if (hasFalse && permissive.length > 0) return { ok: false };
968
+ if (hasFalse) return { ok: true, value: false };
969
+ const unique: unknown[] = [];
970
+ const seen = new Set<string>();
971
+ for (const value of permissive) {
972
+ const key = JSON.stringify(value);
973
+ if (seen.has(key)) continue;
974
+ seen.add(key);
975
+ unique.push(value);
976
+ }
977
+ if (unique.length !== 1) return { ok: false };
978
+ return { ok: true, value: unique[0] };
979
+ }
980
+
981
+ /** Compose root siblings into a branch so properties/required are not overwritten. */
982
+ function composeXaiObjectSchemas(
983
+ inherited: Record<string, unknown>,
984
+ branch: Record<string, unknown>,
985
+ ): Record<string, unknown> {
986
+ const composed: Record<string, unknown> = { ...inherited, ...branch };
987
+ const inheritedProps = isXaiObjectSchema(inherited.properties) ? inherited.properties : undefined;
988
+ const branchProps = isXaiObjectSchema(branch.properties) ? branch.properties : undefined;
989
+ if (inheritedProps || branchProps) {
990
+ const properties: Record<string, unknown> = { ...(inheritedProps ?? {}) };
991
+ for (const [name, value] of Object.entries(branchProps ?? {})) {
992
+ const inheritedValue = inheritedProps?.[name];
993
+ properties[name] = inheritedValue !== undefined && JSON.stringify(inheritedValue) !== JSON.stringify(value)
994
+ ? { allOf: [inheritedValue, value] }
995
+ : value;
996
+ }
997
+ composed.properties = properties;
998
+ }
999
+ const required = [...new Set([
1000
+ ...stringRequiredFields(inherited.required),
1001
+ ...stringRequiredFields(branch.required),
1002
+ ])];
1003
+ if (required.length > 0) composed.required = required;
1004
+ else delete composed.required;
1005
+ return composed;
1006
+ }
1007
+
699
1008
  function expandXaiRootObjectSchemas(schema: unknown): Record<string, unknown>[] | undefined {
700
- if (!schema || typeof schema !== "object" || Array.isArray(schema)) return undefined;
701
- const obj = schema as Record<string, unknown>;
702
- const compositionKey = ["oneOf", "anyOf"].find(key => Array.isArray(obj[key]));
1009
+ if (!isXaiObjectSchema(schema)) return undefined;
1010
+ const compositionKey = ["oneOf", "anyOf"].find(key => Array.isArray(schema[key]));
703
1011
  if (!compositionKey) {
704
- if (obj.type !== undefined && obj.type !== "object") return undefined;
705
- return [{ ...obj, type: "object" }];
1012
+ if (schema.type !== undefined && schema.type !== "object") return undefined;
1013
+ return [{ ...schema, type: "object" }];
706
1014
  }
707
1015
 
708
- const siblings = Object.fromEntries(Object.entries(obj).filter(([key]) => key !== compositionKey));
709
- const branches = obj[compositionKey];
1016
+ const siblings = Object.fromEntries(Object.entries(schema).filter(([key]) => key !== compositionKey));
1017
+ const branches = schema[compositionKey];
710
1018
  if (!Array.isArray(branches)) return undefined;
711
1019
  const expanded: Record<string, unknown>[] = [];
712
1020
  for (const branch of branches) {
713
1021
  const variants = expandXaiRootObjectSchemas(branch);
714
1022
  if (!variants) return undefined;
715
- for (const variant of variants) expanded.push({ ...siblings, ...variant });
1023
+ for (const variant of variants) expanded.push(composeXaiObjectSchemas(siblings, variant));
716
1024
  }
717
1025
  return expanded.length > 0 ? expanded : undefined;
718
1026
  }
719
1027
 
1028
+ function mergeXaiPropertySchemas(values: unknown[]): unknown {
1029
+ const unique: unknown[] = [];
1030
+ const serialized = new Set<string>();
1031
+ for (const value of values) {
1032
+ const key = JSON.stringify(value);
1033
+ if (serialized.has(key)) continue;
1034
+ serialized.add(key);
1035
+ unique.push(value);
1036
+ }
1037
+ return unique.length === 1 ? unique[0] : { anyOf: unique };
1038
+ }
1039
+
1040
+ /**
1041
+ * The Grok CLI proxy rejects a function parameter schema whose root remains oneOf/anyOf.
1042
+ * Flatten only when the merge is lossless: local $refs resolve, every variant is a concrete
1043
+ * object whose keys we can preserve, required sets match, additionalProperties does not change
1044
+ * meaning, every property name exists on every variant, and at most one property schema
1045
+ * differs. Otherwise omit the tool rather than emit a weaker schema.
1046
+ */
720
1047
  function normalizeXaiToolParameters(parameters: unknown): Record<string, unknown> | undefined {
721
- const variants = expandXaiRootObjectSchemas(parameters);
1048
+ if (!isXaiObjectSchema(parameters)) return undefined;
1049
+ const resolved = resolveXaiSchemaRefs(parameters, parameters);
1050
+ if (!isXaiObjectSchema(resolved)) return undefined;
1051
+ const variants = expandXaiRootObjectSchemas(resolved);
722
1052
  if (!variants) return undefined;
723
- if (variants.length === 1) return variants[0];
724
- const root = parameters && typeof parameters === "object" && !Array.isArray(parameters)
725
- ? parameters as Record<string, unknown>
726
- : {};
727
- const metadata = Object.fromEntries(Object.entries(root).filter(([key]) => key !== "oneOf" && key !== "anyOf" && key !== "type"));
728
- return { ...metadata, oneOf: variants };
1053
+ if (variants.length === 1) {
1054
+ return xaiVariantIsConcreteObject(variants[0]) ? variants[0] : undefined;
1055
+ }
1056
+ if (!variants.every(xaiVariantIsConcreteObject) || !xaiRequiredSetsMatch(variants)) return undefined;
1057
+ const additionalProperties = mergeXaiAdditionalProperties(variants);
1058
+ if (!additionalProperties.ok) return undefined;
1059
+ if (!xaiPropertyMergeIsLossless(variants)) return undefined;
1060
+
1061
+ const metadata = Object.fromEntries(Object.entries(resolved).filter(([key]) => key !== "oneOf" && key !== "anyOf" && key !== "type"));
1062
+ delete metadata.properties;
1063
+ delete metadata.required;
1064
+ delete metadata.additionalProperties;
1065
+
1066
+ const propertyValues = new Map<string, unknown[]>();
1067
+ for (const variant of variants) {
1068
+ if (!variant.properties || typeof variant.properties !== "object" || Array.isArray(variant.properties)) continue;
1069
+ for (const [name, value] of Object.entries(variant.properties as Record<string, unknown>)) {
1070
+ const values = propertyValues.get(name) ?? [];
1071
+ values.push(value);
1072
+ propertyValues.set(name, values);
1073
+ }
1074
+ }
1075
+ const properties = Object.fromEntries(
1076
+ [...propertyValues].map(([name, values]) => [name, mergeXaiPropertySchemas(values)]),
1077
+ );
1078
+ const required = stringRequiredFields(variants[0]?.required);
1079
+
1080
+ return {
1081
+ ...metadata,
1082
+ type: "object",
1083
+ properties,
1084
+ ...(required.length > 0 ? { required } : {}),
1085
+ ...("value" in additionalProperties ? { additionalProperties: additionalProperties.value } : {}),
1086
+ };
729
1087
  }
730
1088
 
731
1089
  function toolsToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] | undefined {
@@ -734,9 +1092,9 @@ function toolsToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig
734
1092
  if (tools.length === 0) return undefined;
735
1093
  const xaiTarget = isXaiSchemaTarget(provider);
736
1094
  const formatted = tools.flatMap(t => {
737
- const parameters = xaiTarget
1095
+ const parameters = stripResponsesOnlyEncryptedMarker(xaiTarget
738
1096
  ? normalizeXaiToolParameters(t.parameters)
739
- : ensureRootObjectType(t.parameters);
1097
+ : ensureRootObjectType(t.parameters));
740
1098
 
741
1099
  if (parameters === undefined) return [];
742
1100
  return [{
@@ -825,10 +1183,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
825
1183
  formatErrorBody: formatOpenAIChatErrorBody,
826
1184
 
827
1185
  buildRequest(parsed: OcxParsedRequest) {
828
- const hasCredential = typeof provider.apiKey === "string" && provider.apiKey.trim().length > 0;
829
- if ((provider.authMode === "key" || provider.authMode === "oauth") && !provider.keyOptional && !hasCredential) {
830
- throw new Error(`${provider.adapter} requires a non-empty credential (authMode: ${provider.authMode})`);
831
- }
1186
+ const { url, headers, hasCredential } = openAIChatTransport(provider);
832
1187
 
833
1188
  const messages = messagesToChatFormat(parsed, provider);
834
1189
  const tools = toolsToChatFormatForProvider(parsed, provider);
@@ -844,11 +1199,11 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
844
1199
  // an explicit value here makes the Responses parser's serviceTier projection ineffective.
845
1200
  //
846
1201
  // Opt-in, like `prompt_cache_key` directly below: `service_tier` is an OpenAI-specific
847
- // extension and 66 registry providers share this adapter, several of which reject
848
- // unknown body fields. Forwarding unconditionally would turn a caller-supplied
849
- // `service_tier` into an upstream 400 on those routes. `supportsServiceTier` is the
850
- // Responses-wire flag (applyServiceTierGate) and deliberately does not gate this path.
851
- if (provider.chatServiceTier && parsed.options.serviceTier !== undefined) {
1202
+ // extension and 66 registry providers share this adapter. A provider-wide Chat opt-in
1203
+ // authorizes undeclared models; an exact model declaration can authorize or deny one
1204
+ // model. Provider-level false remains fail-closed.
1205
+ if (canSerializeServiceTierForChatModel(provider, parsed.modelId)
1206
+ && parsed.options.serviceTier !== undefined) {
852
1207
  body.service_tier = parsed.options.serviceTier;
853
1208
  }
854
1209
  if (modelInList(provider.reasoningSplitModels, parsed.modelId)) body.reasoning_split = true;
@@ -981,11 +1336,6 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
981
1336
  }
982
1337
  if (parsed.stream) body.stream_options = { include_usage: true };
983
1338
 
984
- const url = openaiChatCompletionsUrl(provider.baseUrl);
985
- const headers: Record<string, string> = { "Content-Type": "application/json" };
986
- if (hasCredential) headers["Authorization"] = `Bearer ${provider.apiKey}`;
987
- if (provider.headers) Object.assign(headers, provider.headers);
988
-
989
1339
  const bodyJson = JSON.stringify(body);
990
1340
  if (isDebugEnabled()) {
991
1341
  let host = "upstream";
@@ -1153,13 +1503,15 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
1153
1503
  }
1154
1504
  const rawName = rawFunction.name;
1155
1505
  const rawArguments = rawFunction.arguments;
1156
- if ((rawName !== undefined && typeof rawName !== "string")
1157
- || (rawArguments !== undefined && typeof rawArguments !== "string")) {
1506
+ // Some OpenAI-compatible streamers repeat already-sent fields as null on
1507
+ // continuation deltas. Treat only null/undefined as absent; every other
1508
+ // non-string value still fails closed before entering the accumulator.
1509
+ if (isInvalidStreamStringField(rawName) || isInvalidStreamStringField(rawArguments)) {
1158
1510
  logInvalidToolCalls("stream", rawToolCalls);
1159
1511
  return yield* terminateWithError(invalidToolCallsEvent(pendingUsage));
1160
1512
  }
1161
1513
  }
1162
- if (tc.id !== undefined && typeof tc.id !== "string") {
1514
+ if (isInvalidStreamStringField(tc.id)) {
1163
1515
  logInvalidToolCalls("stream", rawToolCalls);
1164
1516
  return yield* terminateWithError(invalidToolCallsEvent(pendingUsage));
1165
1517
  }
@@ -0,0 +1,144 @@
1
+ import { createAnthropicAdapter } from "./anthropic";
2
+ import { createAzureAdapter } from "./azure";
3
+ import type { ProviderAdapter } from "./base";
4
+ import { createCommandCodeAdapter } from "./command-code";
5
+ import { createCursorAdapter } from "./cursor";
6
+ import { createGoogleAdapter } from "./google";
7
+ import { createKiroAdapter } from "./kiro";
8
+ import { createMimoFreeAdapter } from "./mimo-free";
9
+ import { createOpenAIChatAdapter } from "./openai-chat";
10
+ import { createResponsesPassthroughAdapter } from "./openai-responses";
11
+ import type { OcxProviderConfig } from "../types";
12
+
13
+ export type AdapterCacheRetention = "none" | "short" | "long";
14
+
15
+ export interface AdapterFactoryContext {
16
+ cacheRetention?: AdapterCacheRetention;
17
+ }
18
+
19
+ export type AdapterWire =
20
+ | "command-code"
21
+ | "openai-chat"
22
+ | "anthropic"
23
+ | "openai-responses"
24
+ | "google"
25
+ | "kiro"
26
+ | "cursor";
27
+
28
+ export type AdapterMutationContract =
29
+ | "codex-owned"
30
+ | "codex-owned-with-gated-native-fallback";
31
+
32
+ type AdapterFactory = (
33
+ provider: OcxProviderConfig,
34
+ context: AdapterFactoryContext,
35
+ ) => ProviderAdapter;
36
+
37
+ type DirectAdapterDefinition = {
38
+ wire: AdapterWire;
39
+ mutation: AdapterMutationContract;
40
+ create: AdapterFactory;
41
+ };
42
+
43
+ type InheritedAdapterDefinition = {
44
+ /** Semantic contract inheritance only. Runtime construction remains independent. */
45
+ contractParent: string;
46
+ create: AdapterFactory;
47
+ };
48
+
49
+ type AdapterDefinition = DirectAdapterDefinition | InheritedAdapterDefinition;
50
+
51
+ export const ADAPTER_REGISTRY = {
52
+ "command-code": {
53
+ wire: "command-code",
54
+ mutation: "codex-owned",
55
+ create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createCommandCodeAdapter(provider),
56
+ },
57
+ "openai-chat": {
58
+ wire: "openai-chat",
59
+ mutation: "codex-owned",
60
+ create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createOpenAIChatAdapter(provider),
61
+ },
62
+ anthropic: {
63
+ wire: "anthropic",
64
+ mutation: "codex-owned",
65
+ create: (provider: OcxProviderConfig, context: AdapterFactoryContext) =>
66
+ createAnthropicAdapter(provider, context.cacheRetention),
67
+ },
68
+ "openai-responses": {
69
+ wire: "openai-responses",
70
+ mutation: "codex-owned",
71
+ create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) =>
72
+ createResponsesPassthroughAdapter(provider),
73
+ },
74
+ google: {
75
+ wire: "google",
76
+ mutation: "codex-owned",
77
+ create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createGoogleAdapter(provider),
78
+ },
79
+ kiro: {
80
+ wire: "kiro",
81
+ mutation: "codex-owned",
82
+ create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createKiroAdapter(provider),
83
+ },
84
+ azure: {
85
+ contractParent: "openai-responses",
86
+ create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createAzureAdapter(provider),
87
+ },
88
+ "azure-openai": {
89
+ contractParent: "openai-responses",
90
+ create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createAzureAdapter(provider),
91
+ },
92
+ cursor: {
93
+ wire: "cursor",
94
+ mutation: "codex-owned-with-gated-native-fallback",
95
+ create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createCursorAdapter(provider),
96
+ },
97
+ "mimo-free": {
98
+ contractParent: "openai-chat",
99
+ create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createMimoFreeAdapter(provider),
100
+ },
101
+ } as const satisfies Record<string, AdapterDefinition>;
102
+
103
+ export type AdapterId = keyof typeof ADAPTER_REGISTRY;
104
+ export type RegisteredAdapterDefinition = typeof ADAPTER_REGISTRY[AdapterId];
105
+
106
+ export function adapterDefinitions(): Array<[AdapterId, RegisteredAdapterDefinition]> {
107
+ return Object.entries(ADAPTER_REGISTRY) as Array<[AdapterId, RegisteredAdapterDefinition]>;
108
+ }
109
+
110
+ export function getAdapterDefinition(adapterId: unknown): RegisteredAdapterDefinition | undefined {
111
+ if (typeof adapterId !== "string" || !Object.hasOwn(ADAPTER_REGISTRY, adapterId)) return undefined;
112
+ return ADAPTER_REGISTRY[adapterId as AdapterId];
113
+ }
114
+
115
+ export function effectiveAdapterContract(adapterId: string): Readonly<{
116
+ wire: AdapterWire;
117
+ mutation: AdapterMutationContract;
118
+ }> {
119
+ const visited = new Set<string>();
120
+ let current = adapterId;
121
+
122
+ while (true) {
123
+ if (visited.has(current)) {
124
+ throw new Error(`Adapter contract cycle detected at ${current}`);
125
+ }
126
+ visited.add(current);
127
+
128
+ const definition = getAdapterDefinition(current);
129
+ if (!definition) throw new Error(`Unknown adapter: ${current}`);
130
+ if ("wire" in definition) {
131
+ return { wire: definition.wire, mutation: definition.mutation };
132
+ }
133
+ current = definition.contractParent;
134
+ }
135
+ }
136
+
137
+ export function createRegisteredAdapter(
138
+ provider: OcxProviderConfig,
139
+ context: AdapterFactoryContext = {},
140
+ ): ProviderAdapter {
141
+ const definition = getAdapterDefinition(provider.adapter);
142
+ if (!definition) throw new Error(`Unknown adapter: ${provider.adapter}`);
143
+ return definition.create(provider, context);
144
+ }