@bitkyc08/opencodex 2.18.2 → 2.20.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/gui/dist/assets/index-DF_UFrGS.css +1 -0
- package/gui/dist/assets/index-DSK3S5HY.js +76 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/client-fingerprint.ts +14 -10
- package/src/adapters/google-antigravity-wire.ts +4 -3
- package/src/adapters/google.ts +1 -1
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat.ts +367 -32
- package/src/adapters/registry.ts +144 -0
- package/src/chat/inbound.ts +37 -8
- package/src/cli/claude.ts +2 -1
- package/src/cli/combo.ts +3 -0
- package/src/cli/dispatch.ts +8 -0
- package/src/cli/export-command.ts +2 -2
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +3 -2
- package/src/cli/lab.ts +135 -1
- package/src/cli/minimax.ts +491 -0
- package/src/cli/models-runtime.ts +22 -1
- package/src/cli/models.ts +67 -2
- package/src/cli/opencode.ts +2 -1
- package/src/cli/registry.ts +22 -2
- package/src/clients/config-export.ts +125 -7
- package/src/codex/app-server-processes.ts +57 -2
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/catalog/aggregation.ts +10 -1
- package/src/codex/catalog/effort.ts +15 -3
- package/src/codex/catalog/parsing.ts +3 -1
- package/src/codex/catalog/provider-fetch.ts +45 -5
- package/src/codex/catalog/sync.ts +74 -4
- package/src/codex/convergence.ts +2 -0
- package/src/codex/shim.ts +100 -5
- package/src/combos/index.ts +1 -0
- package/src/combos/request.ts +30 -0
- package/src/combos/types.ts +6 -0
- package/src/config.ts +52 -0
- package/src/generated/compatibility-version.json +239 -87
- package/src/images/loop.ts +11 -1
- package/src/integrations/registry.ts +7 -0
- package/src/lab/conformance/jcs.ts +42 -2
- package/src/lab/conformance/negative-controls.ts +6 -2
- package/src/lab/conformance/runner.ts +16 -5
- package/src/lab/fabric/observe.ts +49 -14
- package/src/lab/index.ts +16 -0
- package/src/lab/ledger/purge.ts +152 -83
- package/src/lab/ledger/store.ts +168 -54
- package/src/lab/observe/from-conformance.ts +8 -6
- package/src/lab/observe/from-live.ts +8 -2
- package/src/lab/paths.ts +23 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +219 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/errors.ts +27 -0
- package/src/lib/lab-activation.ts +109 -47
- package/src/lib/lab-live-pinned-sender.ts +16 -5
- package/src/lib/pinned-http.ts +70 -16
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/state-store-registrations.ts +2 -0
- package/src/lib/upstream-reachability.ts +4 -0
- package/src/lib/windows-elevation.ts +10 -1
- package/src/providers/derive.ts +24 -4
- package/src/providers/registry.ts +9 -3
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +143 -0
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/reasoning-effort.ts +27 -1
- package/src/responses/spill-store.ts +20 -1
- package/src/responses/state.ts +159 -3
- package/src/router.ts +23 -6
- package/src/routing/capability.ts +4 -2
- package/src/routing/compatibility/behavior.ts +5 -1
- package/src/server/adapter-resolve.ts +2 -32
- package/src/server/auth-cors.ts +8 -0
- package/src/server/chat-completions.ts +74 -34
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +371 -0
- package/src/server/effort-policy.ts +18 -0
- package/src/server/management/combo-routes.ts +16 -2
- package/src/server/management/config-routes.ts +6 -4
- package/src/server/management/context.ts +17 -0
- package/src/server/management/lab-routes.ts +181 -19
- package/src/server/management/model-routes.ts +76 -2
- package/src/server/management/model-rows.ts +8 -0
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +76 -4
- package/src/server/management/system-restart.ts +4 -2
- package/src/server/management/system-routes.ts +38 -0
- package/src/server/relay.ts +17 -3
- package/src/server/responses/compact.ts +4 -1
- package/src/server/responses/core.ts +278 -48
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/fetch-helpers.ts +35 -4
- package/src/server/responses/input-admission.ts +169 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/policy-fallback.ts +16 -2
- package/src/server/responses/terminal-guard.ts +1 -1
- package/src/server/responses/upstream-error.ts +5 -0
- package/src/server/responses.ts +17 -2
- package/src/service-manager-probe.ts +2 -3
- package/src/types.ts +76 -6
- package/src/update/index.ts +6 -5
- package/src/update/job.ts +5 -6
- package/src/update/notify.ts +5 -3
- package/src/usage/log.ts +11 -1
- package/src/vision/index.ts +21 -4
- package/src/web-search/index.ts +2 -1
- package/gui/dist/assets/index-CXI1262_.js +0 -76
- package/gui/dist/assets/index-DUCH59lJ.css +0 -1
package/src/responses/state.ts
CHANGED
|
@@ -39,6 +39,8 @@ interface ResidentResponseState {
|
|
|
39
39
|
createdAt: number;
|
|
40
40
|
clientThreadId?: string;
|
|
41
41
|
items: unknown[];
|
|
42
|
+
/** Index in `items` where provider output begins; see clientCarriedPrefixLength. */
|
|
43
|
+
providerOutputStart?: number;
|
|
42
44
|
providers?: OcxProviderContinuationState;
|
|
43
45
|
sizeBytes: number;
|
|
44
46
|
}
|
|
@@ -47,6 +49,8 @@ interface SpilledResponseState {
|
|
|
47
49
|
kind: "spill";
|
|
48
50
|
createdAt: number;
|
|
49
51
|
clientThreadId?: string;
|
|
52
|
+
/** Mirrors the spilled payload boundary so a spilled entry keeps its anchor. */
|
|
53
|
+
providerOutputStart?: number;
|
|
50
54
|
providers?: OcxProviderContinuationState;
|
|
51
55
|
spill: ResponseSpillRef;
|
|
52
56
|
sizeBytes: number;
|
|
@@ -130,6 +134,7 @@ function measureResidentEntry(id: string, entry: ResidentInput): ResidentRespons
|
|
|
130
134
|
createdAt: entry.createdAt,
|
|
131
135
|
...(entry.clientThreadId ? { clientThreadId: entry.clientThreadId } : {}),
|
|
132
136
|
items: entry.items,
|
|
137
|
+
...(entry.providerOutputStart !== undefined ? { providerOutputStart: entry.providerOutputStart } : {}),
|
|
133
138
|
...(entry.providers ? { providers: entry.providers } : {}),
|
|
134
139
|
});
|
|
135
140
|
return sizeBytes === null ? null : { kind: "resident", ...entry, sizeBytes };
|
|
@@ -252,12 +257,14 @@ function replaceSpillEntryAtomically(
|
|
|
252
257
|
createdAt: candidate.createdAt,
|
|
253
258
|
...(candidate.clientThreadId ? { clientThreadId: candidate.clientThreadId } : {}),
|
|
254
259
|
items: candidate.items,
|
|
260
|
+
...(candidate.providerOutputStart !== undefined ? { providerOutputStart: candidate.providerOutputStart } : {}),
|
|
255
261
|
...(candidate.providers ? { providers: candidate.providers } : {}),
|
|
256
262
|
});
|
|
257
263
|
const base: Omit<SpilledResponseState, "sizeBytes"> = {
|
|
258
264
|
kind: "spill",
|
|
259
265
|
createdAt: candidate.createdAt,
|
|
260
266
|
...(candidate.clientThreadId ? { clientThreadId: candidate.clientThreadId } : {}),
|
|
267
|
+
...(candidate.providerOutputStart !== undefined ? { providerOutputStart: candidate.providerOutputStart } : {}),
|
|
261
268
|
...(candidate.providers ? { providers: candidate.providers } : {}),
|
|
262
269
|
spill: ref,
|
|
263
270
|
};
|
|
@@ -332,6 +339,7 @@ function admitOversizedCandidate(
|
|
|
332
339
|
createdAt: candidate.createdAt,
|
|
333
340
|
...(candidate.clientThreadId ? { clientThreadId: candidate.clientThreadId } : {}),
|
|
334
341
|
items: candidate.items,
|
|
342
|
+
...(candidate.providerOutputStart !== undefined ? { providerOutputStart: candidate.providerOutputStart } : {}),
|
|
335
343
|
...(candidate.providers ? { providers: candidate.providers } : {}),
|
|
336
344
|
});
|
|
337
345
|
// Enforce the ceiling against the REAL envelope: the spill payload adds
|
|
@@ -373,9 +381,11 @@ function admitOversizedCandidate(
|
|
|
373
381
|
}
|
|
374
382
|
}
|
|
375
383
|
|
|
376
|
-
//
|
|
384
|
+
// Replay provenance must stay proxy-private: a WeakMap distinguishes replayed history from the
|
|
377
385
|
// newly appended input suffix without adding an unknown field that native passthrough could send
|
|
378
|
-
// upstream. The parser uses this boundary to acknowledge historical compaction markers exactly
|
|
386
|
+
// upstream. The parser uses this boundary to acknowledge historical compaction markers exactly
|
|
387
|
+
// once. It records the boundary whether the proxy prepended the history or the client already
|
|
388
|
+
// carried it — the boundary is the same either way, and only its provenance differs.
|
|
379
389
|
const replayedInputPrefixLengths = new WeakMap<object, number>();
|
|
380
390
|
const replayFailures = new WeakMap<object, PreviousResponseReplayFailure>();
|
|
381
391
|
let loaded = false;
|
|
@@ -419,12 +429,23 @@ function loadSnapshotEntry(id: string, value: unknown): void {
|
|
|
419
429
|
const clientThreadId = typeof rec.clientThreadId === "string" && rec.clientThreadId.trim().length > 0
|
|
420
430
|
? rec.clientThreadId.trim()
|
|
421
431
|
: undefined;
|
|
432
|
+
// A malformed boundary degrades to "never skip" rather than to a bad index: an untrusted
|
|
433
|
+
// snapshot must not be able to authorize dropping conversation history.
|
|
434
|
+
const anchorFor = (itemCount: number): number | undefined => {
|
|
435
|
+
const raw = (rec as { providerOutputStart?: unknown }).providerOutputStart;
|
|
436
|
+
return Number.isSafeInteger(raw) && (raw as number) >= 0 && (raw as number) <= itemCount
|
|
437
|
+
? raw as number
|
|
438
|
+
: undefined;
|
|
439
|
+
};
|
|
422
440
|
if (rec.kind === "spill") {
|
|
423
441
|
if (!isSpillRef(rec.spill)) return;
|
|
424
442
|
const base: Omit<SpilledResponseState, "sizeBytes"> = {
|
|
425
443
|
kind: "spill",
|
|
426
444
|
createdAt: rec.createdAt,
|
|
427
445
|
...(clientThreadId ? { clientThreadId } : {}),
|
|
446
|
+
// Item count is unknown until materialization, so accept any non-negative integer
|
|
447
|
+
// here; the spill payload validator re-checks it against the real array.
|
|
448
|
+
...(anchorFor(Number.MAX_SAFE_INTEGER) !== undefined ? { providerOutputStart: anchorFor(Number.MAX_SAFE_INTEGER) } : {}),
|
|
428
449
|
...(rec.providers ? { providers: rec.providers } : {}),
|
|
429
450
|
spill: rec.spill,
|
|
430
451
|
};
|
|
@@ -451,6 +472,7 @@ function loadSnapshotEntry(id: string, value: unknown): void {
|
|
|
451
472
|
createdAt: rec.createdAt,
|
|
452
473
|
...(clientThreadId ? { clientThreadId } : {}),
|
|
453
474
|
items: rec.items,
|
|
475
|
+
...(anchorFor(rec.items.length) !== undefined ? { providerOutputStart: anchorFor(rec.items.length) } : {}),
|
|
454
476
|
...(providers ? { providers } : {}),
|
|
455
477
|
});
|
|
456
478
|
if (!resident) {
|
|
@@ -740,6 +762,94 @@ function inputItems(input: unknown): unknown[] {
|
|
|
740
762
|
return [input];
|
|
741
763
|
}
|
|
742
764
|
|
|
765
|
+
/** Hard cap for canonicalizing ANY item. Past it, the item is not comparable. */
|
|
766
|
+
const REPLAY_FINGERPRINT_MAX_BYTES = 8 * 1024;
|
|
767
|
+
/** Depth ceiling so a pathologically nested item cannot blow the canonicalizer. */
|
|
768
|
+
const REPLAY_FINGERPRINT_MAX_DEPTH = 64;
|
|
769
|
+
|
|
770
|
+
let replayOverlapSkips = 0;
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Canonical, order-stable fingerprint for one input item, or null when the item cannot be
|
|
774
|
+
* compared safely.
|
|
775
|
+
*
|
|
776
|
+
* Byte-counted DURING the walk rather than serialize-then-measure: a tool result can be
|
|
777
|
+
* megabytes and this runs on the request path, so the point of the cap is to stop early,
|
|
778
|
+
* not to discover afterwards that we should have. Object keys are sorted so two
|
|
779
|
+
* semantically identical items cannot differ by key order alone.
|
|
780
|
+
*
|
|
781
|
+
* The cap applies to EVERY item. An `id`/`call_id` is additional occurrence evidence, never
|
|
782
|
+
* a substitute for content equality, so an over-cap identified tool item is non-comparable
|
|
783
|
+
* exactly like an over-cap message.
|
|
784
|
+
*/
|
|
785
|
+
function replayItemFingerprint(item: unknown): string | null {
|
|
786
|
+
const out: string[] = [];
|
|
787
|
+
let bytes = 0;
|
|
788
|
+
const push = (text: string): boolean => {
|
|
789
|
+
bytes += Buffer.byteLength(text, "utf8");
|
|
790
|
+
if (bytes > REPLAY_FINGERPRINT_MAX_BYTES) return false;
|
|
791
|
+
out.push(text);
|
|
792
|
+
return true;
|
|
793
|
+
};
|
|
794
|
+
const walk = (value: unknown, depth: number): boolean => {
|
|
795
|
+
if (depth > REPLAY_FINGERPRINT_MAX_DEPTH) return false;
|
|
796
|
+
if (value === null || typeof value !== "object") return push(JSON.stringify(value) ?? "null");
|
|
797
|
+
if (Array.isArray(value)) {
|
|
798
|
+
if (!push("[")) return false;
|
|
799
|
+
for (const element of value) {
|
|
800
|
+
if (!walk(element, depth + 1)) return false;
|
|
801
|
+
if (!push(",")) return false;
|
|
802
|
+
}
|
|
803
|
+
return push("]");
|
|
804
|
+
}
|
|
805
|
+
if (!push("{")) return false;
|
|
806
|
+
for (const key of Object.keys(value as Record<string, unknown>).sort()) {
|
|
807
|
+
if (!push(JSON.stringify(key))) return false;
|
|
808
|
+
if (!walk((value as Record<string, unknown>)[key], depth + 1)) return false;
|
|
809
|
+
if (!push(",")) return false;
|
|
810
|
+
}
|
|
811
|
+
return push("}");
|
|
812
|
+
};
|
|
813
|
+
return walk(item, 0) ? out.join("") : null;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/** Non-empty provider-issued `id`/`call_id` on an item, else null. */
|
|
817
|
+
function providerIssuedIdentity(item: unknown): string | null {
|
|
818
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
|
819
|
+
const record = item as { id?: unknown; call_id?: unknown };
|
|
820
|
+
for (const candidate of [record.id, record.call_id]) {
|
|
821
|
+
if (typeof candidate === "string" && candidate.trim().length > 0) return candidate;
|
|
822
|
+
}
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Number of leading stored items the client already carries verbatim, or 0.
|
|
828
|
+
*
|
|
829
|
+
* Requires an exact ordered run: every stored item must match the client input item at the
|
|
830
|
+
* same index. Any not-comparable item aborts to 0 — skipping just that item could align two
|
|
831
|
+
* different occurrences and manufacture a false positive, and a false positive here deletes
|
|
832
|
+
* real conversation history.
|
|
833
|
+
*
|
|
834
|
+
* Known gap (FU-2): stored input can contain proxy-injected guidance the client never saw,
|
|
835
|
+
* and ids repaired after recording. Those sessions do not match here and expand as before.
|
|
836
|
+
*/
|
|
837
|
+
function clientCarriedPrefixLength(stored: readonly unknown[], clientInput: readonly unknown[]): number {
|
|
838
|
+
if (stored.length === 0 || clientInput.length < stored.length) return 0;
|
|
839
|
+
for (let index = 0; index < stored.length; index += 1) {
|
|
840
|
+
const storedPrint = replayItemFingerprint(stored[index]);
|
|
841
|
+
if (storedPrint === null) return 0;
|
|
842
|
+
const clientPrint = replayItemFingerprint(clientInput[index]);
|
|
843
|
+
if (clientPrint === null || storedPrint !== clientPrint) return 0;
|
|
844
|
+
}
|
|
845
|
+
return stored.length;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/** Test-only: replay prepends skipped because the client already carried the history. */
|
|
849
|
+
export function replayOverlapSkipsForTests(): number {
|
|
850
|
+
return replayOverlapSkips;
|
|
851
|
+
}
|
|
852
|
+
|
|
743
853
|
function pruneResponses(at = now()): void {
|
|
744
854
|
for (const [id, state] of states) {
|
|
745
855
|
if (at - state.createdAt > RESPONSE_TTL_MS) deleteEntry(id);
|
|
@@ -765,6 +875,7 @@ function pruneResponses(at = now()): void {
|
|
|
765
875
|
createdAt: entry.createdAt,
|
|
766
876
|
...(entry.clientThreadId ? { clientThreadId: entry.clientThreadId } : {}),
|
|
767
877
|
items: entry.items,
|
|
878
|
+
...(entry.providerOutputStart !== undefined ? { providerOutputStart: entry.providerOutputStart } : {}),
|
|
768
879
|
...(entry.providers ? { providers: entry.providers } : {}),
|
|
769
880
|
});
|
|
770
881
|
if (swapResidentForSpill(oldestId, entry, ref)) spillCounters.writes += 1;
|
|
@@ -807,6 +918,7 @@ export function evictOldestResponseContinuationForBudget(): number {
|
|
|
807
918
|
createdAt: entry.createdAt,
|
|
808
919
|
...(entry.clientThreadId ? { clientThreadId: entry.clientThreadId } : {}),
|
|
809
920
|
items: entry.items,
|
|
921
|
+
...(entry.providerOutputStart !== undefined ? { providerOutputStart: entry.providerOutputStart } : {}),
|
|
810
922
|
...(entry.providers ? { providers: entry.providers } : {}),
|
|
811
923
|
});
|
|
812
924
|
if (swapResidentForSpill(id, entry, ref)) spillCounters.writes += 1;
|
|
@@ -848,6 +960,9 @@ function materializeEntry(
|
|
|
848
960
|
createdAt: result.payload.createdAt,
|
|
849
961
|
...(result.payload.clientThreadId ? { clientThreadId: result.payload.clientThreadId } : {}),
|
|
850
962
|
items: result.payload.items,
|
|
963
|
+
...(result.payload.providerOutputStart !== undefined
|
|
964
|
+
? { providerOutputStart: result.payload.providerOutputStart }
|
|
965
|
+
: {}),
|
|
851
966
|
...(result.payload.providers ? { providers: result.payload.providers } : {}),
|
|
852
967
|
});
|
|
853
968
|
if (!state) {
|
|
@@ -892,6 +1007,39 @@ export function expandPreviousResponseInput(body: unknown, clientThreadId?: stri
|
|
|
892
1007
|
replayScopeMismatchDrops += 1;
|
|
893
1008
|
return freshRequest;
|
|
894
1009
|
}
|
|
1010
|
+
// The client already replayed this history verbatim. Prepending the stored copy would
|
|
1011
|
+
// double it, and the doubled turn is stored again, so the next turn triples (#1412 saw
|
|
1012
|
+
// 127k of real context reach 1.3M tokens this way).
|
|
1013
|
+
//
|
|
1014
|
+
// Three conditions, all required. The run must cover the whole stored entry; it must reach
|
|
1015
|
+
// the provider-output region; and some matched item in that region must carry a
|
|
1016
|
+
// provider-issued id. The last one is the load-bearing part: content equality alone proves
|
|
1017
|
+
// two items look alike, not that they are the same occurrence, so a client that merely
|
|
1018
|
+
// repeats its own message would otherwise authorize a skip that deletes real history.
|
|
1019
|
+
// There is no invariant that provider output always carries ids, so an entry whose output
|
|
1020
|
+
// has none simply never skips.
|
|
1021
|
+
{
|
|
1022
|
+
const clientInput = inputItems(request.input);
|
|
1023
|
+
const stored = materialized.state.items;
|
|
1024
|
+
const anchor = materialized.state.providerOutputStart;
|
|
1025
|
+
const carried = clientCarriedPrefixLength(stored, clientInput);
|
|
1026
|
+
if (
|
|
1027
|
+
carried === stored.length
|
|
1028
|
+
&& anchor !== undefined
|
|
1029
|
+
&& carried > anchor
|
|
1030
|
+
&& stored.slice(anchor, carried).some(item => providerIssuedIdentity(item) !== null)
|
|
1031
|
+
) {
|
|
1032
|
+
replayOverlapSkips += 1;
|
|
1033
|
+
// Keep previous_response_id: Kiro and Cursor recover their conversation ids from it
|
|
1034
|
+
// (kiro-wire.ts, cursor/request-builder.ts). Only the concatenation is skipped.
|
|
1035
|
+
const unchanged = { ...request };
|
|
1036
|
+
// Same provenance boundary a real expansion would record, so the replayed prefix does
|
|
1037
|
+
// not re-acknowledge historical compaction markers (parser.ts) and stays visible to
|
|
1038
|
+
// guidance de-duplication (collaboration.ts).
|
|
1039
|
+
replayedInputPrefixLengths.set(unchanged, carried);
|
|
1040
|
+
return unchanged;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
895
1043
|
const expanded = {
|
|
896
1044
|
...request,
|
|
897
1045
|
input: [...materialized.state.items, ...inputItems(request.input)],
|
|
@@ -1044,10 +1192,17 @@ export function rememberResponseState(
|
|
|
1044
1192
|
});
|
|
1045
1193
|
}
|
|
1046
1194
|
const clientThreadId = normalizedClientThreadId(opts?.clientThreadId);
|
|
1195
|
+
// Compute the normalized array once and reuse it for both fields, so the recorded
|
|
1196
|
+
// boundary can never disagree with the items it indexes.
|
|
1197
|
+
const requestItems = inputItems(request.input);
|
|
1047
1198
|
setResidentEntry(response.id, {
|
|
1048
1199
|
createdAt: now(),
|
|
1049
1200
|
...(clientThreadId ? { clientThreadId } : {}),
|
|
1050
|
-
items: [...
|
|
1201
|
+
items: [...requestItems, ...response.output],
|
|
1202
|
+
// Where response.output begins. A replay skip requires a matched item at or past this
|
|
1203
|
+
// index that also carries a provider-issued id — position alone proves only that an item
|
|
1204
|
+
// sits on the provider side, not that the provider authored it.
|
|
1205
|
+
providerOutputStart: requestItems.length,
|
|
1051
1206
|
// Always preserve the Cursor conversation id so the next tool-result turn can continue the SAME
|
|
1052
1207
|
// Cursor conversation (multi-turn continuation). Separately track whether Cursor's own
|
|
1053
1208
|
// checkpoint/cache is safe to reuse: a turn that ended with a pending client tool call produced an
|
|
@@ -1093,6 +1248,7 @@ export function clearResponseStateMemoryForTests(): void {
|
|
|
1093
1248
|
spillCounters.writeFailures = 0;
|
|
1094
1249
|
spillCounters.readFailures = 0;
|
|
1095
1250
|
replayScopeMismatchDrops = 0;
|
|
1251
|
+
replayOverlapSkips = 0;
|
|
1096
1252
|
persistAttemptHookForTests = null;
|
|
1097
1253
|
loaded = false;
|
|
1098
1254
|
}
|
package/src/router.ts
CHANGED
|
@@ -11,8 +11,12 @@ import type { NormalizedComboConfig } from "./combos/types";
|
|
|
11
11
|
import { hasOwnProvider, resolveEnvValue } from "./config";
|
|
12
12
|
import { assertProviderDestinationAllowed } from "./lib/destination-policy";
|
|
13
13
|
import { redactSecretString, redactUrlForLog } from "./lib/redact";
|
|
14
|
-
import { PROVIDER_REGISTRY, providerCodexAccountMode
|
|
14
|
+
import { PROVIDER_REGISTRY, providerCodexAccountMode } from "./providers/registry";
|
|
15
15
|
import { applyDirectReasoningEffortContracts } from "./providers/derive";
|
|
16
|
+
import {
|
|
17
|
+
providerMatchesRegistryTransportWithStaticGuards,
|
|
18
|
+
providerSupportsLiveModelDiscovery,
|
|
19
|
+
} from "./providers/static-model-discovery";
|
|
16
20
|
import {
|
|
17
21
|
isCanonicalOpenAiForwardProvider,
|
|
18
22
|
LEGACY_CHATGPT_PROVIDER_ID,
|
|
@@ -86,7 +90,7 @@ const MODEL_PROVIDER_PATTERNS: Array<{ providerNames: string[]; prefixes: string
|
|
|
86
90
|
export function knownModelIdsForProvider(provName: string, prov: OcxProviderConfig): string[] {
|
|
87
91
|
const ids = new Set<string>();
|
|
88
92
|
for (const id of prov.models ?? []) ids.add(id);
|
|
89
|
-
const registry =
|
|
93
|
+
const registry = providerMatchesRegistryTransportWithStaticGuards(provName, prov)
|
|
90
94
|
? PROVIDER_REGISTRY.find(entry => entry.id === provName)
|
|
91
95
|
: undefined;
|
|
92
96
|
for (const id of registry?.models ?? []) ids.add(id);
|
|
@@ -99,6 +103,7 @@ export function knownModelIdsForProvider(provName: string, prov: OcxProviderConf
|
|
|
99
103
|
registry?.modelDefaultReasoningEfforts,
|
|
100
104
|
registry?.modelReasoningEffortMap,
|
|
101
105
|
registry?.modelMaxOutputTokens,
|
|
106
|
+
registry?.modelSupportsServiceTier,
|
|
102
107
|
]) {
|
|
103
108
|
for (const id of Object.keys(map ?? {})) ids.add(id);
|
|
104
109
|
}
|
|
@@ -249,20 +254,26 @@ function usableResolvedApiKey(apiKey: string | undefined): string | undefined {
|
|
|
249
254
|
|
|
250
255
|
export function routedProviderConfig(providerName: string, provider: OcxProviderConfig): OcxProviderConfig {
|
|
251
256
|
const registryEntry = PROVIDER_REGISTRY.find(entry => entry.id === providerName);
|
|
252
|
-
if (!registryEntry || !
|
|
257
|
+
if (!registryEntry || !providerMatchesRegistryTransportWithStaticGuards(providerName, provider)) {
|
|
253
258
|
assertProviderDestinationAllowed(providerName, provider);
|
|
254
259
|
return { ...provider, apiKey: usableResolvedApiKey(provider.apiKey) };
|
|
255
260
|
}
|
|
256
261
|
const resolvedApiKey = usableResolvedApiKey(provider.apiKey);
|
|
262
|
+
const staticModelCatalog = !providerSupportsLiveModelDiscovery(providerName, provider);
|
|
263
|
+
const repairLegacyMimoFreeAuth = providerName === "mimo-free"
|
|
264
|
+
&& staticModelCatalog
|
|
265
|
+
&& (provider.authMode === undefined || provider.authMode === "local");
|
|
257
266
|
const explicitKeyOverride = registryEntry.authKind === "oauth"
|
|
258
267
|
&& registryEntry.allowKeyAuthOverride === true
|
|
259
268
|
&& provider.authMode === "key"
|
|
260
269
|
&& resolvedApiKey !== undefined;
|
|
261
270
|
const canonicalAuthMode = explicitKeyOverride
|
|
262
271
|
? "key"
|
|
263
|
-
:
|
|
264
|
-
?
|
|
265
|
-
|
|
272
|
+
: repairLegacyMimoFreeAuth
|
|
273
|
+
? "key"
|
|
274
|
+
: registryEntry.authKind === "forward" || registryEntry.authKind === "oauth"
|
|
275
|
+
? registryEntry.authKind
|
|
276
|
+
: provider.authMode === "forward" ? undefined : provider.authMode;
|
|
266
277
|
const reasoningEffortMap = mergeRecord(registryEntry.reasoningEffortMap, provider.reasoningEffortMap);
|
|
267
278
|
const modelReasoningEffortMap = mergeNestedRecord(registryEntry.modelReasoningEffortMap, provider.modelReasoningEffortMap);
|
|
268
279
|
const modelReasoningEfforts = mergeStringArrayRecord(registryEntry.modelReasoningEfforts, provider.modelReasoningEfforts);
|
|
@@ -283,6 +294,10 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
|
|
|
283
294
|
? mergePositiveNumberCaps(registryEntry.modelMaxInputTokens, provider.modelMaxInputTokens)
|
|
284
295
|
: mergeRecordFill(registryEntry.modelMaxInputTokens, provider.modelMaxInputTokens);
|
|
285
296
|
const modelMaxOutputTokens = mergeRecordFill(registryEntry.modelMaxOutputTokens, provider.modelMaxOutputTokens);
|
|
297
|
+
const modelSupportsServiceTier = mergeRecordFill(
|
|
298
|
+
registryEntry.modelSupportsServiceTier,
|
|
299
|
+
provider.modelSupportsServiceTier,
|
|
300
|
+
);
|
|
286
301
|
const noVisionModels = mergeStringArray(registryEntry.noVisionModels, provider.noVisionModels);
|
|
287
302
|
const noReasoningModels = mergeStringArray(registryEntry.noReasoningModels, provider.noReasoningModels);
|
|
288
303
|
const noTemperatureModels = mergeStringArray(registryEntry.noTemperatureModels, provider.noTemperatureModels);
|
|
@@ -343,6 +358,7 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
|
|
|
343
358
|
: {}),
|
|
344
359
|
authMode: canonicalAuthMode,
|
|
345
360
|
apiKey: resolvedApiKey,
|
|
361
|
+
...(staticModelCatalog ? { liveModels: false } : {}),
|
|
346
362
|
// Backfill the Google wire mode + Vertex project/location from the registry when the user
|
|
347
363
|
// config omits them, so a minimal `google-vertex`/`google-antigravity` entry still routes
|
|
348
364
|
// through the correct branch (CCA/Vertex) instead of falling back to AI Studio.
|
|
@@ -372,6 +388,7 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
|
|
|
372
388
|
...(modelInputModalities ? { modelInputModalities } : {}),
|
|
373
389
|
...(modelMaxInputTokens ? { modelMaxInputTokens } : {}),
|
|
374
390
|
...(modelMaxOutputTokens ? { modelMaxOutputTokens } : {}),
|
|
391
|
+
...(modelSupportsServiceTier ? { modelSupportsServiceTier } : {}),
|
|
375
392
|
...(modelReasoningEfforts ? { modelReasoningEfforts } : {}),
|
|
376
393
|
...(modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts } : {}),
|
|
377
394
|
...(reasoningEffortMap ? { reasoningEffortMap } : {}),
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import type { OcxConfig } from "../types";
|
|
14
14
|
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
|
|
15
|
+
import { serviceTierSupportForModel } from "../providers/service-tier";
|
|
15
16
|
import { applyProviderContextCap, providerContextCap } from "../providers/context-cap";
|
|
16
17
|
import { PROVIDER_REGISTRY } from "../providers/registry";
|
|
17
18
|
import {
|
|
@@ -185,8 +186,9 @@ export function candidateCapabilityEvidence(
|
|
|
185
186
|
?? catalogRow?.reasoningEfforts
|
|
186
187
|
?? (isNative ? nativeReasoningEfforts(modelId) : undefined);
|
|
187
188
|
|
|
188
|
-
const tierSupport = provider
|
|
189
|
-
|
|
189
|
+
const tierSupport = provider
|
|
190
|
+
? serviceTierSupportForModel(provider, modelId, providerName)
|
|
191
|
+
: registryEntry ? serviceTierSupportForModel(registryEntry, modelId, providerName) : undefined;
|
|
190
192
|
const serviceTier = tierSupport === true
|
|
191
193
|
? "supported"
|
|
192
194
|
: tierSupport === false ? "unsupported" : "unknown";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { OcxConfig, OcxProviderConfig } from "../../types";
|
|
2
2
|
import { PROVIDER_REGISTRY, type ProviderAuthKind } from "../../providers/registry";
|
|
3
|
+
import { serviceTierSupportForModel } from "../../providers/service-tier";
|
|
3
4
|
import { localFingerprint } from "../../lab/digest";
|
|
4
5
|
import type { LabBehaviorSource, LabBehaviorValues } from "../../lab/live/types";
|
|
5
6
|
|
|
@@ -116,7 +117,10 @@ export function resolveProductionBehaviorValues(
|
|
|
116
117
|
"auth.mode": behaviorRow("provider_config", authMode),
|
|
117
118
|
"auth.transport": behaviorRow("provider_config", authTransportFor(effective, adapter, authMode)),
|
|
118
119
|
"responses.stateful": behaviorRow("provider_config", effective.statelessResponses !== true),
|
|
119
|
-
"responses.serviceTier": behaviorRow(
|
|
120
|
+
"responses.serviceTier": behaviorRow(
|
|
121
|
+
"provider_config",
|
|
122
|
+
serviceTierSupportForModel(effective, modelId, providerName) ?? null,
|
|
123
|
+
),
|
|
120
124
|
"responses.snapshotRepair": behaviorRow("provider_config", effective.responsesSnapshotRepair === true),
|
|
121
125
|
"responses.itemIdRepair": behaviorRow("provider_config", effective.responsesItemIdRepair ?? null),
|
|
122
126
|
"limits.contextWindow": behaviorRow(
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createAzureAdapter } from "../adapters/azure";
|
|
3
|
-
import { createCursorAdapter } from "../adapters/cursor";
|
|
4
|
-
import { createGoogleAdapter } from "../adapters/google";
|
|
5
|
-
import { createKiroAdapter } from "../adapters/kiro";
|
|
6
|
-
import { createMimoFreeAdapter } from "../adapters/mimo-free";
|
|
7
|
-
import { createOpenAIChatAdapter } from "../adapters/openai-chat";
|
|
8
|
-
import { createCommandCodeAdapter } from "../adapters/command-code";
|
|
9
|
-
import { createResponsesPassthroughAdapter } from "../adapters/openai-responses";
|
|
1
|
+
import { createRegisteredAdapter } from "../adapters/registry";
|
|
10
2
|
import type { OcxProviderConfig } from "../types";
|
|
11
3
|
import { isWirePinnedModel, MODEL_ADAPTER_OVERRIDE_ALLOWED, pinnedWireAdapter } from "../types";
|
|
12
4
|
import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
|
|
@@ -57,27 +49,5 @@ export function resolveWireProtocolOverride(
|
|
|
57
49
|
|
|
58
50
|
/** Build the provider adapter for a resolved provider config. */
|
|
59
51
|
export function resolveAdapter(providerConfig: OcxProviderConfig, cacheRetention?: "none" | "short" | "long") {
|
|
60
|
-
|
|
61
|
-
case "command-code":
|
|
62
|
-
return createCommandCodeAdapter(providerConfig);
|
|
63
|
-
case "openai-chat":
|
|
64
|
-
return createOpenAIChatAdapter(providerConfig);
|
|
65
|
-
case "anthropic":
|
|
66
|
-
return createAnthropicAdapter(providerConfig, cacheRetention);
|
|
67
|
-
case "openai-responses":
|
|
68
|
-
return createResponsesPassthroughAdapter(providerConfig);
|
|
69
|
-
case "google":
|
|
70
|
-
return createGoogleAdapter(providerConfig);
|
|
71
|
-
case "kiro":
|
|
72
|
-
return createKiroAdapter(providerConfig);
|
|
73
|
-
case "azure":
|
|
74
|
-
case "azure-openai":
|
|
75
|
-
return createAzureAdapter(providerConfig);
|
|
76
|
-
case "cursor":
|
|
77
|
-
return createCursorAdapter(providerConfig);
|
|
78
|
-
case "mimo-free":
|
|
79
|
-
return createMimoFreeAdapter(providerConfig);
|
|
80
|
-
default:
|
|
81
|
-
throw new Error(`Unknown adapter: ${providerConfig.adapter}`);
|
|
82
|
-
}
|
|
52
|
+
return createRegisteredAdapter(providerConfig, { cacheRetention });
|
|
83
53
|
}
|
package/src/server/auth-cors.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
providerModelCostsConfigError,
|
|
15
15
|
reasoningSummaryDeliveryRecordConfigError,
|
|
16
16
|
retryOn429PolicyConfigError,
|
|
17
|
+
requestPacingConfigError,
|
|
17
18
|
sanitizeModelCostsForDisplay,
|
|
18
19
|
} from "../config";
|
|
19
20
|
import { providerDestinationConfigError } from "../lib/destination-policy";
|
|
@@ -453,6 +454,8 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
|
|
|
453
454
|
// modelCosts is a user-owned display overlay, not part of the canonical
|
|
454
455
|
// forward seed; it is validated separately below (providerModelCostsConfigError).
|
|
455
456
|
delete canonicalCandidate.modelCosts;
|
|
457
|
+
// requestPacing is a user-owned transport overlay, not part of the canonical seed.
|
|
458
|
+
delete canonicalCandidate.requestPacing;
|
|
456
459
|
const canonical = seed && sameCanonicalProviderSeed(canonicalCandidate, seed);
|
|
457
460
|
if (!canonical) {
|
|
458
461
|
return `provider ${name} must equal the canonical built-in provider seed`;
|
|
@@ -477,6 +480,10 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
|
|
|
477
480
|
// it before it reaches the management API response.
|
|
478
481
|
return `provider ${JSON.stringify(redactSecretString(name))} ${retryOn429Error}`;
|
|
479
482
|
}
|
|
483
|
+
const requestPacingError = requestPacingConfigError(raw.requestPacing);
|
|
484
|
+
if (requestPacingError) {
|
|
485
|
+
return `provider ${JSON.stringify(redactSecretString(name))} ${requestPacingError}`;
|
|
486
|
+
}
|
|
480
487
|
const modelCostsError = providerModelCostsConfigError(raw.modelCosts);
|
|
481
488
|
if (modelCostsError) {
|
|
482
489
|
// The provider name is caller-controlled and can be token-shaped; redact and JSON-escape
|
|
@@ -580,6 +587,7 @@ export function safeConfigDTO(config: OcxConfig): unknown {
|
|
|
580
587
|
"keyOptional",
|
|
581
588
|
"freeTier",
|
|
582
589
|
"liveModels",
|
|
590
|
+
"requestPacing",
|
|
583
591
|
"models",
|
|
584
592
|
"contextWindow",
|
|
585
593
|
"modelContextWindows",
|
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
* OpenAI Chat Completions inbound (/v1/chat/completions) for GitHub Copilot App
|
|
3
3
|
* and other OpenAI-compatible clients.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Ordinary openai-chat routes send directly on the Chat Completions wire. Routes
|
|
6
|
+
* that need Responses-only behavior keep the Chat -> Responses -> Chat bridge.
|
|
7
7
|
*/
|
|
8
8
|
import { FORWARD_HEADERS } from "../adapters/openai-responses";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
assertChatCompletionsRoutingBody,
|
|
11
|
+
ChatCompletionsRequestError,
|
|
12
|
+
chatCompletionsToResponsesBody,
|
|
13
|
+
} from "../chat/inbound";
|
|
10
14
|
import {
|
|
11
15
|
chatCompletionsErrorResponse,
|
|
12
16
|
collectChatCompletion,
|
|
@@ -40,6 +44,7 @@ import {
|
|
|
40
44
|
isTranslatorBudgetExceededError,
|
|
41
45
|
type TranslatorBudget,
|
|
42
46
|
} from "../lib/translator-budget";
|
|
47
|
+
import { handleNativeChatCompletions, isNativeChatRouteEligible } from "./chat-native";
|
|
43
48
|
|
|
44
49
|
type Rec = Record<string, unknown>;
|
|
45
50
|
|
|
@@ -80,11 +85,11 @@ async function handleChatCompletionsWithBudget(
|
|
|
80
85
|
translatorBudget: TranslatorBudget,
|
|
81
86
|
logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease },
|
|
82
87
|
): Promise<Response> {
|
|
83
|
-
let chatBody:
|
|
84
|
-
let internalBody: Rec;
|
|
88
|
+
let chatBody: Rec;
|
|
85
89
|
try {
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
const rawBody = await readChatBody(req, translatorBudget);
|
|
91
|
+
assertChatCompletionsRoutingBody(rawBody);
|
|
92
|
+
chatBody = rawBody;
|
|
88
93
|
} catch (err) {
|
|
89
94
|
const overflow = isTranslatorBudgetExceededError(err);
|
|
90
95
|
const status = overflow ? 413 : err instanceof ChatCompletionsRequestError ? 400 : 500;
|
|
@@ -97,20 +102,17 @@ async function handleChatCompletionsWithBudget(
|
|
|
97
102
|
);
|
|
98
103
|
}
|
|
99
104
|
|
|
100
|
-
const requestedModel =
|
|
101
|
-
const stream =
|
|
105
|
+
const requestedModel = chatBody.model as string;
|
|
106
|
+
const stream = chatBody.stream === true;
|
|
102
107
|
// Best-effort Grok attribution: the managed fence stamps this header on every model
|
|
103
108
|
// it registers (extra_headers, sent verbatim by upstream Grok). Dashboard usage
|
|
104
109
|
// bucketing only — never an auth or billing signal.
|
|
105
110
|
if (req.headers.get("x-opencodex-grok") === "1") logCtx.surface = "grok";
|
|
106
|
-
// Routed adapters only support streamed turns; always stream internally and fold
|
|
107
|
-
// for non-streaming clients.
|
|
108
|
-
internalBody.stream = true;
|
|
109
|
-
|
|
110
|
-
let nativeRoute = false;
|
|
111
111
|
let directRoute = false;
|
|
112
|
+
let settledRoute: ReturnType<typeof routeModel> | null = null;
|
|
113
|
+
let chatNativeRoute: ReturnType<typeof routeModel> | null = null;
|
|
112
114
|
try {
|
|
113
|
-
const route = routeModel(config,
|
|
115
|
+
const route = routeModel(config, requestedModel, evidenceFromBody(chatBody));
|
|
114
116
|
// Settle the wire once so every branch below reads the adapter this model will
|
|
115
117
|
// actually use, not the provider-wide default (#404).
|
|
116
118
|
route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, "chat");
|
|
@@ -119,31 +121,17 @@ async function handleChatCompletionsWithBudget(
|
|
|
119
121
|
logCtx.requestedModel = requestedModel;
|
|
120
122
|
logCtx.provider = route.providerName;
|
|
121
123
|
logCtx.routeDecision = route.routeDecision;
|
|
124
|
+
settledRoute = route;
|
|
122
125
|
if (route.provider.adapter === "openai-responses") {
|
|
123
|
-
nativeRoute = true;
|
|
124
126
|
directRoute = route.codexAccountMode === "direct";
|
|
125
|
-
// ChatGPT backend rejects store:true and unsupported sampling knobs.
|
|
126
|
-
internalBody.store = false;
|
|
127
|
-
delete internalBody.max_output_tokens;
|
|
128
|
-
delete internalBody.temperature;
|
|
129
|
-
delete internalBody.top_p;
|
|
130
|
-
delete internalBody.stop;
|
|
131
|
-
delete internalBody.user;
|
|
132
|
-
} else if (internalBody.store === undefined) {
|
|
133
|
-
internalBody.store = false;
|
|
134
127
|
}
|
|
135
128
|
if (route.provider.adapter === "cursor" || route.provider.adapter === "kiro") {
|
|
136
|
-
const raw = chatBody as Rec;
|
|
137
129
|
const parts: string[] = [];
|
|
138
|
-
if (
|
|
139
|
-
if (
|
|
130
|
+
if (chatBody.messages !== undefined) parts.push(JSON.stringify(chatBody.messages));
|
|
131
|
+
if (chatBody.tools !== undefined) parts.push(JSON.stringify(chatBody.tools));
|
|
140
132
|
logCtx.usageLogInputTokens = Math.max(1, estimateTokens(parts.join("\n"), requestedModel));
|
|
141
133
|
}
|
|
142
|
-
if (
|
|
143
|
-
const { supportedLadderFor } = await import("./effort-policy");
|
|
144
|
-
const ladder = supportedLadderFor({ provider: route.provider, modelId: route.modelId });
|
|
145
|
-
if (ladder !== undefined && ladder.length === 0) delete internalBody.reasoning;
|
|
146
|
-
}
|
|
134
|
+
if (isNativeChatRouteEligible(route, chatBody)) chatNativeRoute = route;
|
|
147
135
|
} catch (err) {
|
|
148
136
|
if (err instanceof NoEligiblePolicyCandidateError) {
|
|
149
137
|
logCtx.routeDecision = err.trace;
|
|
@@ -152,7 +140,59 @@ async function handleChatCompletionsWithBudget(
|
|
|
152
140
|
}
|
|
153
141
|
/* unknown model: let handleResponses shape the 404 */
|
|
154
142
|
}
|
|
155
|
-
|
|
143
|
+
|
|
144
|
+
if (chatNativeRoute) {
|
|
145
|
+
return handleNativeChatCompletions({
|
|
146
|
+
req,
|
|
147
|
+
config,
|
|
148
|
+
logCtx,
|
|
149
|
+
...(logIds ? { logIds } : {}),
|
|
150
|
+
route: chatNativeRoute,
|
|
151
|
+
chatBody,
|
|
152
|
+
requestedModel,
|
|
153
|
+
requestedStream: stream,
|
|
154
|
+
translatorBudget,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let internalBody: Rec;
|
|
159
|
+
try {
|
|
160
|
+
// Validate the full Chat boundary after routing. Native Chat keeps `chatBody` as
|
|
161
|
+
// its wire source; this Responses projection is used only by the fallback path.
|
|
162
|
+
internalBody = chatCompletionsToResponsesBody(chatBody);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
const overflow = isTranslatorBudgetExceededError(err);
|
|
165
|
+
const status = overflow ? 413 : err instanceof ChatCompletionsRequestError ? 400 : 500;
|
|
166
|
+
if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, { closeReason: "non_stream" });
|
|
167
|
+
return chatCompletionsErrorResponse(
|
|
168
|
+
status,
|
|
169
|
+
overflow ? "request translation buffer exceeded the safe limit" : err instanceof Error ? err.message : String(err),
|
|
170
|
+
overflow ? "request_too_large" : undefined,
|
|
171
|
+
overflow ? "translation_buffer_limit" : undefined,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Routed adapters only support streamed turns; always stream internally and fold
|
|
176
|
+
// for non-streaming clients. Native Chat uses the caller's original stream bit.
|
|
177
|
+
internalBody.stream = true;
|
|
178
|
+
if (settledRoute?.provider.adapter === "openai-responses") {
|
|
179
|
+
// ChatGPT backend rejects store:true and unsupported sampling knobs.
|
|
180
|
+
internalBody.store = false;
|
|
181
|
+
delete internalBody.max_output_tokens;
|
|
182
|
+
delete internalBody.temperature;
|
|
183
|
+
delete internalBody.top_p;
|
|
184
|
+
delete internalBody.stop;
|
|
185
|
+
delete internalBody.user;
|
|
186
|
+
} else if (internalBody.store === undefined) {
|
|
187
|
+
internalBody.store = false;
|
|
188
|
+
}
|
|
189
|
+
if (settledRoute && internalBody.reasoning !== undefined) {
|
|
190
|
+
const { stripEmptyLadderEffort, supportedLadderFor } = await import("./effort-policy");
|
|
191
|
+
const ladder = supportedLadderFor({ provider: settledRoute.provider, modelId: settledRoute.modelId });
|
|
192
|
+
const next = stripEmptyLadderEffort(internalBody.reasoning, ladder);
|
|
193
|
+
if (next === undefined) delete internalBody.reasoning;
|
|
194
|
+
else internalBody.reasoning = next;
|
|
195
|
+
}
|
|
156
196
|
|
|
157
197
|
const headers = new Headers({ "content-type": "application/json" });
|
|
158
198
|
for (const name of FORWARD_HEADERS) {
|