@bitkyc08/opencodex 2.10.0 → 2.10.1
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/AGENTS_INSTALL.md +77 -0
- package/README.md +4 -10
- package/bin/ocx.mjs +71 -18
- package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
- package/gui/dist/assets/index-ChZQsmBY.js +70 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +8 -4
- package/src/adapters/anthropic.ts +208 -14
- package/src/adapters/base.ts +16 -5
- package/src/adapters/cursor/effort-map.ts +3 -2
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/live-transport.ts +105 -95
- package/src/adapters/cursor/native-exec.ts +32 -6
- package/src/adapters/cursor/protobuf-request.ts +20 -15
- package/src/adapters/cursor/request-builder.ts +21 -7
- package/src/adapters/cursor/types.ts +7 -0
- package/src/adapters/google-antigravity-replay.ts +237 -21
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +50 -9
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +10 -2
- package/src/adapters/kiro-tools.ts +10 -1
- package/src/adapters/kiro.ts +37 -11
- package/src/adapters/openai-chat.ts +284 -83
- package/src/adapters/openai-responses.ts +182 -24
- package/src/bridge.ts +177 -7
- package/src/chat/outbound.ts +78 -23
- package/src/claude/agents-inject.ts +27 -5
- package/src/claude/inbound.ts +11 -1
- package/src/claude/model-info.ts +13 -10
- package/src/claude/outbound.ts +17 -0
- package/src/cli/account-api.ts +24 -0
- package/src/cli/account-auth.ts +31 -6
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +5 -0
- package/src/cli/claude.ts +2 -1
- package/src/cli/doctor.ts +93 -22
- package/src/cli/export-command.ts +26 -12
- package/src/cli/help.ts +8 -6
- package/src/cli/index.ts +56 -22
- package/src/cli/integrations.ts +84 -1
- package/src/cli/observe.ts +54 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +1 -1
- package/src/cli/v2.ts +36 -9
- package/src/clients/config-export.ts +687 -10
- package/src/codex/account-lifecycle.ts +30 -5
- package/src/codex/account-usability.ts +22 -2
- package/src/codex/admission.ts +255 -0
- package/src/codex/auth-api.ts +427 -140
- package/src/codex/auth-context.ts +155 -30
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +62 -0
- package/src/codex/catalog/aggregation.ts +14 -1
- package/src/codex/catalog/bundled.ts +282 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/metadata.ts +51 -6
- package/src/codex/catalog/parsing.ts +6 -3
- package/src/codex/catalog/provider-fetch.ts +576 -41
- package/src/codex/catalog/sync.ts +505 -66
- package/src/codex/catalog-admission.ts +197 -0
- package/src/codex/catalog-write-serialization.ts +241 -0
- package/src/codex/catalog.ts +2 -1
- package/src/codex/codex-write-lock.ts +372 -0
- package/src/codex/convergence-types.ts +593 -0
- package/src/codex/convergence.ts +441 -0
- package/src/codex/desired-state.ts +177 -0
- package/src/codex/features.ts +52 -8
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +257 -0
- package/src/codex/history-lock.ts +241 -0
- package/src/codex/history-migration-guardian.ts +18 -5
- package/src/codex/history-provider.ts +9 -2
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +176 -0
- package/src/codex/inject-coordination.ts +245 -0
- package/src/codex/inject.ts +605 -124
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +10 -1
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/management-convergence.ts +114 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +167 -0
- package/src/codex/native-main-lock-file.ts +158 -0
- package/src/codex/native-main-owner.ts +315 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1512 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +340 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +557 -0
- package/src/codex/project-config-warnings.ts +18 -4
- package/src/codex/prompt-journal.ts +311 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +224 -0
- package/src/codex/quota.ts +86 -3
- package/src/codex/routing.ts +299 -62
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +39 -13
- package/src/codex/subagent-model-fallback.ts +73 -12
- package/src/codex/transition-state.ts +604 -0
- package/src/codex/upstream-host-health.ts +70 -0
- package/src/codex/user-identity.ts +266 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/config.ts +562 -26
- package/src/generated/jawcode-model-metadata.ts +2 -2
- package/src/grok/inject.ts +15 -4
- package/src/grok/inspect.ts +45 -0
- package/src/images/loop.ts +113 -20
- package/src/integrations/config-io.ts +151 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/native/ownership-preflight.ts +165 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +101 -0
- package/src/integrations/serialize.ts +235 -0
- package/src/integrations/state.ts +290 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer.ts +492 -0
- package/src/lib/bounded-body.ts +46 -8
- package/src/lib/bun-runtime.ts +110 -1
- package/src/lib/bun-stream-caps.ts +2 -1
- package/src/lib/redact.ts +407 -2
- package/src/lib/shadow-call.ts +24 -0
- package/src/lib/translator-budget.ts +10 -0
- package/src/lib/upstream-reachability.ts +91 -0
- package/src/lib/upstream-retry.ts +154 -2
- package/src/lib/windows-secret-acl.ts +212 -11
- package/src/lib/winsw.ts +9 -3
- package/src/oauth/index.ts +61 -3
- package/src/oauth/key-providers.ts +4 -0
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/store.ts +31 -6
- package/src/oauth/token-guardian.ts +9 -3
- package/src/providers/codex-capacity.ts +288 -0
- package/src/providers/derive.ts +33 -1
- package/src/providers/free-directory.ts +3 -1
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/openai-sidecar.ts +64 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota.ts +334 -26
- package/src/providers/registry.ts +284 -16
- package/src/providers/xai-transport.ts +11 -4
- package/src/responses/compaction.ts +8 -1
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/parser.ts +44 -2
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +105 -0
- package/src/responses/spill-store.ts +45 -8
- package/src/responses/state.ts +161 -17
- package/src/router.ts +302 -16
- package/src/routing/analytics.ts +377 -0
- package/src/routing/capability.ts +204 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +444 -0
- package/src/routing/health.ts +401 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +590 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile.ts +423 -0
- package/src/routing/quota.ts +124 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +686 -0
- package/src/server/auth-cors.ts +46 -6
- package/src/server/chat-completions.ts +28 -13
- package/src/server/claude-messages.ts +23 -15
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +10 -1
- package/src/server/index.ts +238 -52
- package/src/server/lifecycle.ts +228 -9
- package/src/server/live.ts +14 -3
- package/src/server/management/agent-settings-routes.ts +64 -14
- package/src/server/management/combo-routes.ts +5 -5
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +42 -2
- package/src/server/management/integration-routes.ts +538 -0
- package/src/server/management/logs-usage-routes.ts +1 -1
- package/src/server/management/model-routes.ts +32 -113
- package/src/server/management/model-rows.ts +117 -0
- package/src/server/management/native-integration-routes.ts +587 -0
- package/src/server/management/oauth-account-routes.ts +1 -1
- package/src/server/management/provider-routes.ts +218 -117
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +177 -0
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +276 -24
- package/src/server/management/system-routes.ts +4 -0
- package/src/server/management-api.ts +84 -9
- package/src/server/management-auth.ts +43 -5
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +120 -6
- package/src/server/request-log.ts +26 -6
- package/src/server/responses/collaboration.ts +63 -8
- package/src/server/responses/compact.ts +272 -41
- package/src/server/responses/core.ts +730 -132
- package/src/server/responses/fetch-helpers.ts +15 -1
- package/src/server/responses-item-id-repair.ts +32 -3
- package/src/server/responses-json-events.ts +52 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/search.ts +51 -6
- package/src/server/sse-payload-rewrite.ts +89 -12
- package/src/server/startup-health-cache.ts +7 -1
- package/src/server/ws-bridge.ts +11 -17
- package/src/service-manager-probe.ts +297 -0
- package/src/service.ts +222 -32
- package/src/tray/windows-tray.ps1 +9 -0
- package/src/tray/windows.ts +15 -7
- package/src/types.ts +194 -14
- package/src/update/index.ts +13 -13
- package/src/update/job.ts +24 -21
- package/src/update/notify.ts +7 -3
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +129 -10
- package/src/usage/log.ts +50 -15
- package/src/usage/summary.ts +4 -4
- package/src/vision/index.ts +6 -1
- package/src/web-search/loop.ts +161 -34
- package/gui/dist/assets/index-OY43ubAq.css +0 -1
- package/gui/dist/assets/index-YwNnKZcL.js +0 -67
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -34,6 +35,8 @@ const REPLAY_MAX_CALLS_PER_SESSION = 256;
|
|
|
34
35
|
export const ANTIGRAVITY_REPLAY_MAX_BYTES_PER_SESSION = 2 * 1024 * 1024;
|
|
35
36
|
export const ANTIGRAVITY_REPLAY_MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
36
37
|
const REPLAY_MAX_SIGNATURE_BYTES = 64 * 1024;
|
|
38
|
+
/** Fixed 64-hex outer key length, counted once per session entry. */
|
|
39
|
+
const REPLAY_SESSION_KEY_BYTES = 64;
|
|
37
40
|
|
|
38
41
|
interface ReplayLimits {
|
|
39
42
|
maxCallsPerSession: number;
|
|
@@ -54,29 +57,202 @@ let replayBytes = 0;
|
|
|
54
57
|
let replayOldestSessionKey: string | undefined;
|
|
55
58
|
let replayOldestAt: number | null = null;
|
|
56
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Fixed-size identity for a (model, sessionId) pair: SHA-256 over
|
|
62
|
+
* length-prefixed UTF-16 code units fed incrementally (no separator ambiguity
|
|
63
|
+
* — `("a\0b","c")` and `("a","b\0c")` derive different keys — and no raw
|
|
64
|
+
* model/session strings retained as Map keys, which the byte caps never
|
|
65
|
+
* counted).
|
|
66
|
+
*/
|
|
67
|
+
/**
|
|
68
|
+
* Injective string feed for key derivation: length-prefixed in CODE UNITS,
|
|
69
|
+
* then each code unit as two little-endian bytes. TextEncoder/UTF-8 would
|
|
70
|
+
* fold lone surrogates into U+FFFD, colliding distinct strings (e.g.
|
|
71
|
+
* "�" and "�") into the same key.
|
|
72
|
+
*/
|
|
73
|
+
function updateHashWithString(hash: ReturnType<typeof createHash>, value: string): void {
|
|
74
|
+
hash.update(String(value.length));
|
|
75
|
+
hash.update("\0");
|
|
76
|
+
const buf = Buffer.allocUnsafe(8192);
|
|
77
|
+
let offset = 0;
|
|
78
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
79
|
+
buf.writeUInt16LE(value.charCodeAt(index), offset);
|
|
80
|
+
offset += 2;
|
|
81
|
+
if (offset === buf.length) {
|
|
82
|
+
hash.update(buf);
|
|
83
|
+
offset = 0;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (offset > 0) hash.update(buf.subarray(0, offset));
|
|
87
|
+
}
|
|
88
|
+
|
|
57
89
|
function replayKey(model: string, sessionId: string): string {
|
|
58
|
-
|
|
90
|
+
const hash = createHash("sha256");
|
|
91
|
+
updateHashWithString(hash, model);
|
|
92
|
+
updateHashWithString(hash, sessionId);
|
|
93
|
+
return hash.digest("hex");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Canonical output exceeding this budget is rejected DURING the walk — the
|
|
97
|
+
* pre-fix path materialized an unbounded canonical string before admission. */
|
|
98
|
+
const REPLAY_MAX_CANONICAL_ARGS_BYTES = 64 * 1024;
|
|
99
|
+
const CANONICAL_OVERFLOW = Symbol("canonical-overflow");
|
|
100
|
+
|
|
101
|
+
/** Byte-identical output to the old recursive canonicalJson, written incrementally. */
|
|
102
|
+
/**
|
|
103
|
+
* Key-count pre-check: every object key costs at least 4 canonical bytes
|
|
104
|
+
* (two quotes, colon, separator), so a wider object ALWAYS overflows the
|
|
105
|
+
* canonical budget — skip the sort and the walk. Object.keys allocation
|
|
106
|
+
* itself is linear and irreducible in JS (there is no streaming key API),
|
|
107
|
+
* but it is transient and never sorted or walked past this bound.
|
|
108
|
+
*/
|
|
109
|
+
const CANONICAL_MAX_KEYS_PER_OBJECT = REPLAY_MAX_CANONICAL_ARGS_BYTES / 4;
|
|
110
|
+
|
|
111
|
+
/** Test-only scan instrumentation: proves overflow aborts the walk near the
|
|
112
|
+
* cap instead of scanning/materializing the whole input. */
|
|
113
|
+
let canonicalScanUnitsForTests = 0;
|
|
114
|
+
export function canonicalScanUnitsForTestsValue(): number {
|
|
115
|
+
return canonicalScanUnitsForTests;
|
|
59
116
|
}
|
|
117
|
+
export function resetCanonicalScanUnitsForTests(): void {
|
|
118
|
+
canonicalScanUnitsForTests = 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const MAX_CANONICAL_DEPTH = 128;
|
|
60
122
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
123
|
+
function writeCanonicalJson(value: unknown, sink: (chunk: string) => void, depth = 0): void {
|
|
124
|
+
canonicalScanUnitsForTests += 1;
|
|
125
|
+
// Depth overflow is the same class as byte overflow: skip replay for this
|
|
126
|
+
// call instead of exhausting the stack on a pathological argument shape.
|
|
127
|
+
if (depth > MAX_CANONICAL_DEPTH) throw CANONICAL_OVERFLOW;
|
|
128
|
+
if (typeof value === "string") {
|
|
129
|
+
writeJsonStringEscaped(value, sink);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (value === null || typeof value !== "object") {
|
|
133
|
+
sink(JSON.stringify(value) ?? "null");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (Array.isArray(value)) {
|
|
137
|
+
sink("[");
|
|
138
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
139
|
+
if (index > 0) sink(",");
|
|
140
|
+
// Array.prototype.map parity: holes produce NOTHING between the commas
|
|
141
|
+
// (old output `[1,,3]`), while an explicit undefined element is "null".
|
|
142
|
+
if (index in value) writeCanonicalJson(value[index], sink, depth + 1);
|
|
143
|
+
}
|
|
144
|
+
sink("]");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const keys = Object.keys(value as Record<string, unknown>);
|
|
148
|
+
if (keys.length > CANONICAL_MAX_KEYS_PER_OBJECT) throw CANONICAL_OVERFLOW;
|
|
149
|
+
keys.sort();
|
|
150
|
+
sink("{");
|
|
151
|
+
keys.forEach((k, index) => {
|
|
152
|
+
if (index > 0) sink(",");
|
|
153
|
+
writeJsonStringEscaped(k, sink);
|
|
154
|
+
sink(":");
|
|
155
|
+
writeCanonicalJson((value as Record<string, unknown>)[k], sink, depth + 1);
|
|
156
|
+
});
|
|
157
|
+
sink("}");
|
|
68
158
|
}
|
|
69
159
|
|
|
70
|
-
/**
|
|
160
|
+
/**
|
|
161
|
+
* JSON.stringify string escaping, streamed in small chunks so the budget can
|
|
162
|
+
* reject mid-string — calling JSON.stringify on a multi-MiB primitive would
|
|
163
|
+
* materialize its full escaped form before the sink could refuse it.
|
|
164
|
+
* Semantics mirror JSON.stringify for strings exactly (ES2019 JSON
|
|
165
|
+
* superset): quotes/backslash and control characters are escaped, LONE
|
|
166
|
+
* surrogates become \uXXXX, and valid surrogate pairs pass through raw.
|
|
167
|
+
*/
|
|
168
|
+
function writeJsonStringEscaped(value: string, sink: (chunk: string) => void): void {
|
|
169
|
+
sink('"');
|
|
170
|
+
let buffer = "";
|
|
171
|
+
for (const cp of value) {
|
|
172
|
+
canonicalScanUnitsForTests += 1;
|
|
173
|
+
const code = cp.codePointAt(0)!;
|
|
174
|
+
let escaped: string;
|
|
175
|
+
if (cp === '"') escaped = '\\"';
|
|
176
|
+
else if (cp === "\\") escaped = "\\\\";
|
|
177
|
+
else if (cp === "\b") escaped = "\\b";
|
|
178
|
+
else if (cp === "\f") escaped = "\\f";
|
|
179
|
+
else if (cp === "\n") escaped = "\\n";
|
|
180
|
+
else if (cp === "\r") escaped = "\\r";
|
|
181
|
+
else if (cp === "\t") escaped = "\\t";
|
|
182
|
+
else if (code < 0x20) escaped = `\\u${code.toString(16).padStart(4, "0")}`;
|
|
183
|
+
// Lone surrogates: JSON.stringify emits \uXXXX (a raw one would decode
|
|
184
|
+
// back as U+FFFD and collide with real U+FFFD content).
|
|
185
|
+
else if (code >= 0xd800 && code <= 0xdfff) escaped = `\\u${code.toString(16).padStart(4, "0")}`;
|
|
186
|
+
else escaped = cp;
|
|
187
|
+
buffer += escaped;
|
|
188
|
+
if (buffer.length >= 4096) {
|
|
189
|
+
sink(buffer);
|
|
190
|
+
buffer = "";
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (buffer.length > 0) sink(buffer);
|
|
194
|
+
sink('"');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Bounded canonicalization: null on overflow (skip replay for that call). */
|
|
198
|
+
function canonicalJsonBounded(value: unknown, maxBytes: number): string | null {
|
|
199
|
+
let written = 0;
|
|
200
|
+
const parts: string[] = [];
|
|
201
|
+
const sink = (chunk: string) => {
|
|
202
|
+
written += utf8.encode(chunk).byteLength;
|
|
203
|
+
if (written > maxBytes) throw CANONICAL_OVERFLOW;
|
|
204
|
+
parts.push(chunk);
|
|
205
|
+
};
|
|
206
|
+
try {
|
|
207
|
+
writeCanonicalJson(value, sink);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (error === CANONICAL_OVERFLOW) return null;
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
return parts.join("");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Stable identity for a functionCall part: fixed-size SHA-256 over
|
|
217
|
+
* length-prefixed name + canonical args. Overflow during canonicalization
|
|
218
|
+
* skips replay for that call (never materializes an unbounded string); other
|
|
219
|
+
* canonicalization failures keep the old name-only fallback semantics.
|
|
220
|
+
*/
|
|
71
221
|
function functionCallKey(name: unknown, args: unknown): string | undefined {
|
|
72
222
|
if (typeof name !== "string" || name.length === 0) return undefined;
|
|
73
|
-
let
|
|
223
|
+
let canonical: string | null;
|
|
74
224
|
try {
|
|
75
|
-
|
|
225
|
+
canonical = canonicalJsonBounded(args ?? {}, REPLAY_MAX_CANONICAL_ARGS_BYTES);
|
|
76
226
|
} catch {
|
|
77
|
-
|
|
227
|
+
canonical = "";
|
|
78
228
|
}
|
|
79
|
-
return
|
|
229
|
+
if (canonical === null) return undefined;
|
|
230
|
+
const hash = createHash("sha256");
|
|
231
|
+
updateHashWithString(hash, name);
|
|
232
|
+
updateHashWithString(hash, canonical);
|
|
233
|
+
return hash.digest("hex");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Test-only key-derivation seam: the fixed-key regression cannot go red
|
|
237
|
+
* through snapshot.bytes (that metric never counted raw outer keys). */
|
|
238
|
+
export function antigravityReplayKeyForTests(model: string, sessionId: string): string {
|
|
239
|
+
return replayKey(model, sessionId);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function antigravityFunctionCallKeyForTests(name: unknown, args: unknown): string | undefined {
|
|
243
|
+
return functionCallKey(name, args);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Test-only bounded-canonicalization seam: proves mid-walk rejection without
|
|
247
|
+
* materializing the escaped form (allocation guard). */
|
|
248
|
+
export function antigravityCanonicalJsonBoundedForTests(value: unknown, maxBytes: number): string | null {
|
|
249
|
+
return canonicalJsonBounded(value, maxBytes);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Test-only: the ACTUAL internal session keys, so tests can prove raw
|
|
253
|
+
* model/session strings are never retained as Map keys. */
|
|
254
|
+
export function antigravityReplaySessionKeysForTests(): string[] {
|
|
255
|
+
return [...replayCache.keys()];
|
|
80
256
|
}
|
|
81
257
|
|
|
82
258
|
function extractSignature(part: Record<string, unknown>): string | undefined {
|
|
@@ -123,6 +299,22 @@ function deleteExpiredReplaySessions(now: number): void {
|
|
|
123
299
|
for (const [key, entry] of replayCache) if (entry.expiresAtMs <= now) deleteReplaySession(key);
|
|
124
300
|
}
|
|
125
301
|
|
|
302
|
+
/**
|
|
303
|
+
* The lazy per-call expiry scan is O(sessions); at the 10,240-session cap
|
|
304
|
+
* every observe/apply would rescan the whole map — O(n²) under load. The 60s
|
|
305
|
+
* state-store sweeper is already the periodic expiry authority, so lazy scans
|
|
306
|
+
* are throttled to at most one per interval (expired entries may linger a few
|
|
307
|
+
* extra seconds; TTL is fuzzy at that scale by design).
|
|
308
|
+
*/
|
|
309
|
+
const LAZY_SWEEP_INTERVAL_MS = 30_000;
|
|
310
|
+
let lastLazySweepAt = Number.NEGATIVE_INFINITY;
|
|
311
|
+
|
|
312
|
+
function deleteExpiredReplaySessionsThrottled(now: number): void {
|
|
313
|
+
if (now - lastLazySweepAt < LAZY_SWEEP_INTERVAL_MS) return;
|
|
314
|
+
lastLazySweepAt = now;
|
|
315
|
+
deleteExpiredReplaySessions(now);
|
|
316
|
+
}
|
|
317
|
+
|
|
126
318
|
export function sweepExpiredAntigravityReplay(now = Date.now()): number {
|
|
127
319
|
const before = replayCache.size;
|
|
128
320
|
deleteExpiredReplaySessions(now);
|
|
@@ -173,39 +365,62 @@ export function antigravityUsesReplayCache(model: string): boolean {
|
|
|
173
365
|
* Observe a parsed CCA chunk's `candidates[0].content.parts` and record thought signatures keyed by
|
|
174
366
|
* the functionCall identity (name + args). Accumulates across the whole session so a sequential
|
|
175
367
|
* multi-step tool loop keeps EVERY prior call's signature, not just the latest part-index slot.
|
|
368
|
+
* A signature on a standalone thought part is paired with the next functionCall in the same
|
|
369
|
+
* array (#897); a call's own signature takes precedence and an unpaired one is dropped.
|
|
176
370
|
* `parts` is the already-unwrapped `response.candidates[0].content.parts`.
|
|
177
371
|
*/
|
|
178
372
|
export function observeAntigravityReplay(model: string, sessionId: string, parts: unknown[]): void {
|
|
179
373
|
if (!antigravityUsesReplayCache(model) || !Array.isArray(parts) || parts.length === 0) return;
|
|
180
374
|
const now = Date.now();
|
|
181
|
-
|
|
375
|
+
deleteExpiredReplaySessionsThrottled(now);
|
|
182
376
|
const key = replayKey(model, sessionId);
|
|
183
|
-
const
|
|
377
|
+
const existing = replayCache.get(key);
|
|
378
|
+
const entry = existing ?? {
|
|
184
379
|
byCall: new Map<string, ReplayCall>(),
|
|
185
|
-
bytes:
|
|
380
|
+
bytes: REPLAY_SESSION_KEY_BYTES,
|
|
186
381
|
expiresAtMs: 0,
|
|
187
382
|
oldestAtMs: null,
|
|
188
383
|
};
|
|
189
384
|
let inserted = false;
|
|
385
|
+
let pendingThoughtSig: string | undefined;
|
|
190
386
|
for (const raw of parts) {
|
|
191
387
|
if (!raw || typeof raw !== "object") continue;
|
|
192
388
|
const part = raw as Record<string, unknown>;
|
|
193
389
|
const sig = extractSignature(part);
|
|
194
|
-
if (!sig) continue;
|
|
195
390
|
const fc = part.functionCall as { name?: unknown; args?: unknown } | undefined;
|
|
196
|
-
|
|
391
|
+
if (!fc) {
|
|
392
|
+
// A signature on a standalone thought part pairs with the NEXT functionCall in this
|
|
393
|
+
// array: thought parts are stripped from replayed history, so the call part is the only
|
|
394
|
+
// carrier that survives (#897). Non-thought parts keep their own signature in history.
|
|
395
|
+
if (sig && part.thought === true) pendingThoughtSig = sig;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
const callSig = sig ?? pendingThoughtSig; // a signature on the call part itself wins
|
|
399
|
+
pendingThoughtSig = undefined;
|
|
400
|
+
if (!callSig) continue;
|
|
401
|
+
const ck = functionCallKey(fc.name, fc.args);
|
|
197
402
|
if (!ck) continue; // only function-call signatures are replayable by identity
|
|
198
|
-
const signatureBytes = utf8.encode(
|
|
403
|
+
const signatureBytes = utf8.encode(callSig).byteLength;
|
|
199
404
|
const sizeBytes = utf8.encode(ck).byteLength + signatureBytes;
|
|
200
405
|
if (signatureBytes > replayLimits.maxSignatureBytes || sizeBytes > replayLimits.maxBytesPerSession) continue;
|
|
201
406
|
deleteReplayCall(entry, ck);
|
|
202
|
-
entry.byCall.set(ck, { signature:
|
|
407
|
+
entry.byCall.set(ck, { signature: callSig, sizeBytes, touchedAtMs: now });
|
|
203
408
|
entry.bytes += sizeBytes;
|
|
204
409
|
replayBytes += sizeBytes;
|
|
205
410
|
inserted = true;
|
|
206
411
|
}
|
|
207
412
|
if (!inserted) return;
|
|
413
|
+
// Charge the fixed outer key only when the session is actually stored.
|
|
414
|
+
if (!existing) replayBytes += REPLAY_SESSION_KEY_BYTES;
|
|
208
415
|
evictInnerCalls(entry);
|
|
416
|
+
if (entry.byCall.size === 0) {
|
|
417
|
+
// The fixed session overhead can exceed the per-session cap on its own
|
|
418
|
+
// (test-sized limits): an entry holding zero calls is unusable — drop it
|
|
419
|
+
// instead of retaining an unevictable shell.
|
|
420
|
+
if (existing) deleteReplaySession(key);
|
|
421
|
+
else replayBytes -= REPLAY_SESSION_KEY_BYTES;
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
209
424
|
entry.expiresAtMs = now + REPLAY_TTL_MS;
|
|
210
425
|
replayCache.set(key, entry);
|
|
211
426
|
refreshReplaySessionCandidate(key, entry);
|
|
@@ -221,7 +436,7 @@ export function observeAntigravityReplay(model: string, sessionId: string, parts
|
|
|
221
436
|
export function applyAntigravityReplay(model: string, sessionId: string, contents: unknown[]): unknown[] {
|
|
222
437
|
if (!antigravityUsesReplayCache(model) || !Array.isArray(contents)) return contents;
|
|
223
438
|
const now = Date.now();
|
|
224
|
-
|
|
439
|
+
deleteExpiredReplaySessionsThrottled(now);
|
|
225
440
|
const entry = replayCache.get(replayKey(model, sessionId));
|
|
226
441
|
if (!entry) {
|
|
227
442
|
return contents;
|
|
@@ -296,6 +511,7 @@ export function setAntigravityReplayLimitsForTests(limits?: Partial<ReplayLimits
|
|
|
296
511
|
|
|
297
512
|
/** Test seam. */
|
|
298
513
|
export function __resetAntigravityReplayCache(): void {
|
|
514
|
+
lastLazySweepAt = Number.NEGATIVE_INFINITY;
|
|
299
515
|
replayCache.clear();
|
|
300
516
|
replayBytes = 0;
|
|
301
517
|
replayOldestSessionKey = undefined;
|
|
@@ -11,3 +11,14 @@ export function vertexTruncationErrorMessage(reason?: string): string {
|
|
|
11
11
|
const suffix = reason ? ` (${redactSecretString(reason).slice(0, 160)})` : "";
|
|
12
12
|
return `Vertex AI response truncated upstream before the turn completed${suffix}`;
|
|
13
13
|
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Whether a finished turn must fail closed. A truncation reason arriving mid tool call always
|
|
17
|
+
* does. MALFORMED_FUNCTION_CALL fails closed even with zero started calls: the malformed call
|
|
18
|
+
* is dropped upstream and usually never materializes as a part, so the turn is incomplete
|
|
19
|
+
* despite looking empty. MAX_TOKENS with no started call stays a plain token-limit stop.
|
|
20
|
+
*/
|
|
21
|
+
export function isVertexTruncatedTurn(finishReason: string | undefined, toolCallsStarted: number): boolean {
|
|
22
|
+
if (!isVertexTruncationReason(finishReason)) return false;
|
|
23
|
+
return toolCallsStarted > 0 || finishReason === "MALFORMED_FUNCTION_CALL";
|
|
24
|
+
}
|
package/src/adapters/google.ts
CHANGED
|
@@ -12,15 +12,15 @@ import type {
|
|
|
12
12
|
OcxToolCall,
|
|
13
13
|
OcxUsage,
|
|
14
14
|
} from "../types";
|
|
15
|
-
import { isAllowedToolChoice, namespacedToolName, toolAllowedByChoice } from "../types";
|
|
15
|
+
import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolAllowedByChoice } from "../types";
|
|
16
16
|
import { contentPartsToText, parseDataUrl } from "./image";
|
|
17
17
|
import { getVertexAccessToken } from "../lib/gcp-adc";
|
|
18
18
|
import { fetchAntigravityWithRetry, fetchVertexWithRetry } from "./google-http";
|
|
19
19
|
import { safeAntigravityHttpErrorMessage, safeVertexHttpErrorMessage } from "./google-errors";
|
|
20
|
-
import {
|
|
20
|
+
import { isVertexTruncatedTurn, vertexTruncationErrorMessage } from "./google-truncation";
|
|
21
21
|
import { ANTIGRAVITY_REQUEST_UA, antigravitySessionId, isLikelyRealThoughtSignature, sanitizeAntigravityClaudeSignatures } from "./google-antigravity-wire";
|
|
22
22
|
import { compileGoogleWireBody } from "./google-wire-compiler";
|
|
23
|
-
import {
|
|
23
|
+
import { identifyRoutedModel } from "./identity";
|
|
24
24
|
import { antigravityUsesReplayCache, applyAntigravityReplay, clearAntigravityReplay, observeAntigravityReplay } from "./google-antigravity-replay";
|
|
25
25
|
import { resolveAntigravityEffortWireModel } from "../providers/antigravity-models";
|
|
26
26
|
import {
|
|
@@ -122,15 +122,18 @@ function geminiToolResultText(content: string | OcxContentPart[]): string {
|
|
|
122
122
|
return hasContent ? contentPartsToText(content) : GEMINI_EMPTY_TOOL_OUTPUT_PLACEHOLDER;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
function messagesToGeminiFormat(
|
|
125
|
+
function messagesToGeminiFormat(
|
|
126
|
+
parsed: OcxParsedRequest,
|
|
127
|
+
routedModelId = parsed.modelId,
|
|
128
|
+
): { systemInstruction?: unknown; contents: unknown[] } {
|
|
126
129
|
// Neutralize Codex's GPT-5 identity line (Gemini/Antigravity share this path) so a routed model
|
|
127
130
|
// never misreports as GPT-5/OpenAI, and never leaks the proxy identity upstream.
|
|
128
131
|
const toolCatalogNudge = buildNonOpenAIToolCatalogNudgeForTools(parsed.context.tools, parsed.options.toolChoice);
|
|
129
|
-
const systemText =
|
|
132
|
+
const systemText = identifyRoutedModel([
|
|
130
133
|
...(parsed.context.systemPrompt ?? []),
|
|
131
134
|
...(toolCatalogNudge ? [toolCatalogNudge] : []),
|
|
132
135
|
GOOGLE_BREVITY_INSTRUCTION,
|
|
133
|
-
].join("\n\n"));
|
|
136
|
+
].join("\n\n"), routedModelId);
|
|
134
137
|
const systemInstruction = { parts: [{ text: systemText }] };
|
|
135
138
|
|
|
136
139
|
const contents: unknown[] = [];
|
|
@@ -229,6 +232,28 @@ function toolsToGeminiFormat(parsed: OcxParsedRequest): unknown[] | undefined {
|
|
|
229
232
|
}];
|
|
230
233
|
}
|
|
231
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Client tool_choice enforcement on the wire. The catalog nudge states the same contract in
|
|
237
|
+
* prose, but without functionCallingConfig the model is free to ignore it. "auto" stays absent
|
|
238
|
+
* so the common case is byte-identical. The allowedTools variant already filters the
|
|
239
|
+
* declarations in toolsToGeminiFormat; only its "required" half needs a wire mode.
|
|
240
|
+
*/
|
|
241
|
+
function toolChoiceToGeminiToolConfig(parsed: OcxParsedRequest): Record<string, unknown> | undefined {
|
|
242
|
+
const choice = parsed.options.toolChoice;
|
|
243
|
+
if (!choice || choice === "auto") return undefined;
|
|
244
|
+
if (choice === "none") return { functionCallingConfig: { mode: "NONE" } };
|
|
245
|
+
if (choice === "required") return { functionCallingConfig: { mode: "ANY" } };
|
|
246
|
+
if (isAllowedToolChoice(choice)) {
|
|
247
|
+
return choice.mode === "required" ? { functionCallingConfig: { mode: "ANY" } } : undefined;
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
functionCallingConfig: {
|
|
251
|
+
mode: "ANY",
|
|
252
|
+
allowedFunctionNames: [resolveToolChoiceWireName(parsed.context.tools, choice.name)],
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
232
257
|
function usageFromGemini(usage: Record<string, number> | undefined): OcxUsage | undefined {
|
|
233
258
|
if (!usage) return undefined;
|
|
234
259
|
return {
|
|
@@ -292,12 +317,22 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
292
317
|
: {}),
|
|
293
318
|
|
|
294
319
|
async buildRequest(parsed: OcxParsedRequest) {
|
|
295
|
-
const
|
|
320
|
+
const routedModelId = provider.googleMode === "cloud-code-assist"
|
|
321
|
+
? resolveAntigravityEffortWireModel(
|
|
322
|
+
parsed.modelId,
|
|
323
|
+
mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning),
|
|
324
|
+
).wireModelId
|
|
325
|
+
: parsed.modelId;
|
|
326
|
+
const { systemInstruction, contents } = messagesToGeminiFormat(parsed, routedModelId);
|
|
296
327
|
const tools = toolsToGeminiFormat(parsed);
|
|
297
328
|
|
|
298
329
|
const body: Record<string, unknown> = { contents };
|
|
299
330
|
if (systemInstruction) body.systemInstruction = systemInstruction;
|
|
300
331
|
if (tools) body.tools = tools;
|
|
332
|
+
// Only meaningful with declarations on the wire: mode ANY with an empty
|
|
333
|
+
// catalog is a guaranteed upstream 400.
|
|
334
|
+
const toolConfig = tools ? toolChoiceToGeminiToolConfig(parsed) : undefined;
|
|
335
|
+
if (toolConfig) body.toolConfig = toolConfig;
|
|
301
336
|
|
|
302
337
|
const generationConfig: Record<string, unknown> = {};
|
|
303
338
|
if (parsed.options.maxOutputTokens) generationConfig.maxOutputTokens = parsed.options.maxOutputTokens;
|
|
@@ -349,6 +384,12 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
349
384
|
const draftRequest: Record<string, unknown> = { ...body, sessionId };
|
|
350
385
|
// Claude-on-Antigravity forces VALIDATED function calling (the real client always sets it).
|
|
351
386
|
if (/claude/i.test(wireModelId)) {
|
|
387
|
+
// VALIDATED would defeat a client's tool_choice "none": honor it by dropping the
|
|
388
|
+
// declarations instead, the wire shape of a tool-less Claude turn.
|
|
389
|
+
if (parsed.options.toolChoice === "none") {
|
|
390
|
+
delete draftRequest.tools;
|
|
391
|
+
delete draftRequest.toolConfig;
|
|
392
|
+
}
|
|
352
393
|
const existing = (draftRequest.toolConfig ?? {}) as Record<string, unknown>;
|
|
353
394
|
const fcc = (existing.functionCallingConfig ?? {}) as Record<string, unknown>;
|
|
354
395
|
draftRequest.toolConfig = { ...existing, functionCallingConfig: { ...fcc, mode: "VALIDATED" } };
|
|
@@ -587,7 +628,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
587
628
|
// Fail-closed: a turn cut off mid tool call (MAX_TOKENS / MALFORMED_FUNCTION_CALL) surfaces
|
|
588
629
|
// an error instead of a silently-incomplete done. Mirrors kiro-truncation.
|
|
589
630
|
if ((provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist")
|
|
590
|
-
&&
|
|
631
|
+
&& isVertexTruncatedTurn(lastFinishReason, toolCallsStarted)) {
|
|
591
632
|
yield { type: "error", message: vertexTruncationErrorMessage(lastFinishReason) };
|
|
592
633
|
return;
|
|
593
634
|
}
|
|
@@ -743,7 +784,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
743
784
|
// Fail-closed truncation, same as the stream path: a non-stream turn cut off mid tool call
|
|
744
785
|
// (MAX_TOKENS / MALFORMED_FUNCTION_CALL) surfaces an error instead of a silent done.
|
|
745
786
|
if ((provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist")
|
|
746
|
-
&&
|
|
787
|
+
&& isVertexTruncatedTurn(candidates?.[0]?.finishReason, toolCallsStarted)) {
|
|
747
788
|
return finish([{ type: "error", message: vertexTruncationErrorMessage(candidates?.[0]?.finishReason) }]);
|
|
748
789
|
}
|
|
749
790
|
|
package/src/adapters/identity.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Central identity
|
|
2
|
+
* Central routed-model identity repair.
|
|
3
3
|
*
|
|
4
4
|
* Codex sends the SAME GPT-5 identity line to EVERY model at request time (the per-model catalog
|
|
5
5
|
* `base_instructions` is ignored on the wire). For routed, non-OpenAI providers that line is both
|
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
* into the upstream payload — a signature no first-party client (Claude Code, Gemini CLI, Kiro) ever
|
|
9
9
|
* sends, and a likely ToS trigger.
|
|
10
10
|
*
|
|
11
|
-
* The
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* to be a specific
|
|
11
|
+
* The replacement keeps the necessary instruction (don't misreport as GPT-5/OpenAI), names the
|
|
12
|
+
* model id that is actually sent on the wire when it is safe to interpolate, and names no proxy.
|
|
13
|
+
* Provider-native identity blocks (e.g. the anthropic OAuth "You are a Claude agent..." prefix)
|
|
14
|
+
* are layered on TOP of this by the individual adapters; this module never claims to be a specific
|
|
15
|
+
* first-party client.
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
/** Historical exact identity line Codex injected for every model. */
|
|
@@ -37,7 +38,39 @@ export const NEUTRAL_IDENTITY_LINE = "You are a coding agent. Do not claim to be
|
|
|
37
38
|
* the leak can't reappear in one adapter while being fixed in another.
|
|
38
39
|
*/
|
|
39
40
|
export function neutralizeIdentity(systemText: string): string {
|
|
40
|
-
|
|
41
|
+
// A callback avoids `$&`, `$'`, and other replacement-string substitutions if this constant ever
|
|
42
|
+
// becomes configurable. Keep the same safe form in identifyRoutedModel below.
|
|
43
|
+
return systemText.replace(CODEX_GPT5_IDENTITY_RE, () => NEUTRAL_IDENTITY_LINE);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function safeRoutedModelIdentity(modelName: string): string | null {
|
|
47
|
+
// Callers pass the model id after adapter-specific wire normalization. Brackets remain valid for
|
|
48
|
+
// providers that intentionally send a suffix such as `[1m]`; the OpenAI-chat adapter strips that
|
|
49
|
+
// suffix before calling us only when modelSuffixBracketStrip is enabled.
|
|
50
|
+
const trimmed = modelName.trim();
|
|
51
|
+
if (trimmed.length === 0 || trimmed.length > 128) return null;
|
|
52
|
+
const allowedPunctuation = "._/@:+-[]~";
|
|
53
|
+
for (const char of trimmed) {
|
|
54
|
+
const code = char.charCodeAt(0);
|
|
55
|
+
const isAsciiAlphaNumeric = (code >= 48 && code <= 57)
|
|
56
|
+
|| (code >= 65 && code <= 90)
|
|
57
|
+
|| (code >= 97 && code <= 122);
|
|
58
|
+
if (!isAsciiAlphaNumeric && !allowedPunctuation.includes(char)) return null;
|
|
59
|
+
}
|
|
60
|
+
return trimmed;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Identity for a routed model. Callers pass the concrete model id that will be sent upstream, so
|
|
65
|
+
* identity questions can name it instead of falling back to Codex/GPT identity inherited from the
|
|
66
|
+
* native template.
|
|
67
|
+
*/
|
|
68
|
+
export function identifyRoutedModel(systemText: string, modelName: string): string {
|
|
69
|
+
const identity = safeRoutedModelIdentity(modelName);
|
|
70
|
+
const replacement = identity
|
|
71
|
+
? `You are a coding agent powered by the ${identity}. If asked which model you are, identify as ${identity}. Do not claim to be a different model or to have a different creator.`
|
|
72
|
+
: "You are a coding agent powered by the configured model. If asked which model you are, identify as configured model. Do not claim to be GPT-5 or made by OpenAI.";
|
|
73
|
+
return systemText.replace(CODEX_GPT5_IDENTITY_RE, () => replacement);
|
|
41
74
|
}
|
|
42
75
|
|
|
43
76
|
/** The catalog (static, on-disk) replacement for `base_instructions`. Same neutral wording. */
|
|
@@ -109,6 +109,17 @@ function classifyKiroFailure(
|
|
|
109
109
|
retryable: false,
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
|
+
// #993: a gated model demanding a profileArn gets a stable, actionable code
|
|
113
|
+
// instead of the generic validation bucket. Non-retryable by definition.
|
|
114
|
+
if (evidence.includes("profilearn") && evidence.includes("required")) {
|
|
115
|
+
return {
|
|
116
|
+
message: "kiro_profile_required: Kiro requires a CodeWhisperer profileArn for this account and model. Re-login or re-import the matching Kiro account (ocx account login kiro --reauth) so the profile is captured, then retry.",
|
|
117
|
+
status: 400,
|
|
118
|
+
errorType: "invalid_request_error",
|
|
119
|
+
code: "kiro_profile_required",
|
|
120
|
+
retryable: false,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
112
123
|
if (
|
|
113
124
|
evidence.includes("insufficient_quota")
|
|
114
125
|
|| evidence.includes("quota exhausted")
|
|
@@ -3,7 +3,8 @@ import { kiroTruncationReason } from "./kiro-truncation";
|
|
|
3
3
|
|
|
4
4
|
export type ParsedKiroEvent =
|
|
5
5
|
| { type: "content"; data?: string; modelId?: string }
|
|
6
|
-
| { type: "reasoning"; data?: string }
|
|
6
|
+
| { type: "reasoning"; data?: string; redactedContent?: string }
|
|
7
|
+
| { type: "context_usage"; contextUsagePercentage: number }
|
|
7
8
|
| { type: "tool"; name?: string; toolUseId?: string; input?: string; stop?: boolean }
|
|
8
9
|
| { type: "truncation"; data: string }
|
|
9
10
|
| { type: "metadata"; usage?: OcxUsage; contextUsagePercentage?: number; stopReason?: string }
|
|
@@ -17,6 +18,10 @@ const KNOWN_EVENT_TYPES = new Set([
|
|
|
17
18
|
"toolUseEvent",
|
|
18
19
|
"messageMetadataEvent",
|
|
19
20
|
"metadataEvent",
|
|
21
|
+
// Authoritative context pressure. Every capture (kiro-cli 2.14.1 and 2.16.0) put the percentage
|
|
22
|
+
// HERE and left `metadataEvent` carrying only `stopReason`; metadataEvent's own
|
|
23
|
+
// contextUsagePercentage stays supported as a fallback rather than being dropped.
|
|
24
|
+
"contextUsageEvent",
|
|
20
25
|
"invalidStateEvent",
|
|
21
26
|
"error",
|
|
22
27
|
]);
|
|
@@ -114,11 +119,17 @@ export function parseKiroEvent(eventType: string, payload: Uint8Array): ParsedKi
|
|
|
114
119
|
: {}),
|
|
115
120
|
};
|
|
116
121
|
case "reasoningContentEvent":
|
|
122
|
+
// `text` is plaintext reasoning; `redactedContent` is the encrypted blob the GPT-5.6 family
|
|
123
|
+
// (sol/terra/luna) actually returns — they never send `text`. Keyed off the wire field, not
|
|
124
|
+
// the model id. Both may be absent on a bare event.
|
|
117
125
|
return {
|
|
118
126
|
type: "reasoning",
|
|
119
127
|
...(optionalString(eventType, parsed, "text") !== undefined
|
|
120
128
|
? { data: optionalString(eventType, parsed, "text") }
|
|
121
129
|
: {}),
|
|
130
|
+
...(optionalString(eventType, parsed, "redactedContent") !== undefined
|
|
131
|
+
? { redactedContent: optionalString(eventType, parsed, "redactedContent") }
|
|
132
|
+
: {}),
|
|
122
133
|
};
|
|
123
134
|
case "toolUseEvent":
|
|
124
135
|
return {
|
|
@@ -161,6 +172,13 @@ export function parseKiroEvent(eventType: string, payload: Uint8Array): ParsedKi
|
|
|
161
172
|
...(stopReason !== undefined ? { stopReason } : {}),
|
|
162
173
|
};
|
|
163
174
|
}
|
|
175
|
+
case "contextUsageEvent": {
|
|
176
|
+
const contextUsagePercentage = parsed.contextUsagePercentage;
|
|
177
|
+
if (typeof contextUsagePercentage !== "number" || !Number.isFinite(contextUsagePercentage)) {
|
|
178
|
+
return malformed(eventType, "contextUsagePercentage must be a finite number");
|
|
179
|
+
}
|
|
180
|
+
return { type: "context_usage", contextUsagePercentage };
|
|
181
|
+
}
|
|
164
182
|
case "invalidStateEvent":
|
|
165
183
|
return { type: "invalid_state", message: optionalString(eventType, parsed, "message") };
|
|
166
184
|
case "error":
|
|
@@ -89,8 +89,16 @@ export class KiroThinkingParser {
|
|
|
89
89
|
return events;
|
|
90
90
|
}
|
|
91
91
|
if (this.thinkingBuffer.length <= MAX_CLOSE_TAG) return [];
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
// Never split a surrogate pair at the send boundary: a lone high
|
|
93
|
+
// surrogate at the end of one delta encodes as U+FFFD. Move the cut one
|
|
94
|
+
// unit earlier so the whole pair stays in the carry.
|
|
95
|
+
let cut = this.thinkingBuffer.length - MAX_CLOSE_TAG;
|
|
96
|
+
if (cut > 0 && cut < this.thinkingBuffer.length) {
|
|
97
|
+
const atCut = this.thinkingBuffer.charCodeAt(cut - 1);
|
|
98
|
+
if (atCut >= 0xd800 && atCut <= 0xdbff) cut -= 1;
|
|
99
|
+
}
|
|
100
|
+
const send = this.thinkingBuffer.slice(0, cut);
|
|
101
|
+
this.replaceCarry("thinkingBuffer", this.thinkingBuffer.slice(cut));
|
|
94
102
|
return send ? [{ type: "reasoning_raw_delta", text: send }] : [];
|
|
95
103
|
}
|
|
96
104
|
}
|
|
@@ -149,7 +149,16 @@ function toolDescriptionLimit(modelId: string): number {
|
|
|
149
149
|
function truncateDescription(description: string, limit: number): string {
|
|
150
150
|
if (description.length <= limit) return description;
|
|
151
151
|
if (limit <= 1) return description.slice(0, limit);
|
|
152
|
-
|
|
152
|
+
let end = limit - 1;
|
|
153
|
+
// Never end the kept text on a lone high surrogate; one step back keeps
|
|
154
|
+
// the whole pair out instead of a U+FFFD-producing half.
|
|
155
|
+
if (description.charCodeAt(end - 1) >= 0xd800 && description.charCodeAt(end - 1) <= 0xdbff) end -= 1;
|
|
156
|
+
return `${description.slice(0, end)}…`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Test-only: exercise the surrogate-safe description truncation directly. */
|
|
160
|
+
export function truncateDescriptionForTests(description: string, limit: number): string {
|
|
161
|
+
return truncateDescription(description, limit);
|
|
153
162
|
}
|
|
154
163
|
|
|
155
164
|
function serializedToolCatalogBytes(tools: readonly unknown[]): number {
|