@coseung2/opencodex 2.8.0-cs.15 → 2.8.0-cs.17
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-BhXIu7c0.js → index-Ch-99jy3.js} +2 -2
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/packages/ocx-notch/README.md +3 -1
- package/src/adapters/base.ts +12 -0
- package/src/adapters/identity.ts +1 -1
- package/src/adapters/kiro-calibration.ts +83 -0
- package/src/adapters/kiro-constants.ts +11 -2
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +18 -2
- package/src/adapters/kiro-tools.ts +12 -3
- package/src/adapters/kiro.ts +300 -78
- package/src/adapters/openai-chat.ts +1 -42
- package/src/adapters/openai-responses.ts +126 -9
- package/src/adapters/xai-schema-analysis.ts +78 -0
- package/src/adapters/xai-tool-schema.ts +274 -0
- package/src/adapters/xai-web-search.ts +138 -0
- package/src/bridge.ts +61 -6
- package/src/cli/observe.ts +18 -3
- package/src/codex/app-server-processes.ts +3 -5
- package/src/codex/catalog/effort.ts +4 -2
- package/src/codex/catalog/metadata.ts +42 -9
- package/src/codex/catalog/parsing.ts +17 -2
- package/src/codex/catalog/provider-fetch.ts +9 -3
- package/src/codex/catalog/sync.ts +11 -5
- package/src/codex/data/upstream-models.json +169 -0
- package/src/grok/inject.ts +1 -1
- package/src/lib/errors.ts +18 -0
- package/src/lib/token-estimate.ts +42 -38
- package/src/lib/translator-budget.ts +34 -0
- package/src/oauth/index.ts +10 -4
- package/src/oauth/kiro.ts +71 -6
- package/src/oauth/store.ts +3 -1
- package/src/oauth/types.ts +4 -0
- package/src/providers/derive.ts +7 -5
- package/src/providers/opencode-go-transport.ts +59 -0
- package/src/providers/quota.ts +68 -60
- package/src/providers/registry.ts +41 -10
- package/src/providers/xai-transport.ts +10 -0
- package/src/responses/compaction.ts +8 -1
- package/src/responses/namespace-aliases.ts +56 -0
- package/src/responses/parser.ts +12 -0
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/snapshot-policy.ts +108 -0
- package/src/responses/state.ts +23 -10
- package/src/responses/turn-termination.ts +108 -0
- package/src/responses/xai-custom-tool-compat.ts +237 -0
- package/src/server/grok-responses-snapshot-repair.ts +338 -0
- package/src/server/index.ts +2 -1
- package/src/server/relay-eager.ts +1 -0
- package/src/server/request-log-conversation.ts +8 -0
- package/src/server/request-log.ts +5 -4
- package/src/server/responses/core.ts +233 -16
- package/src/server/responses-image-gen-repair.ts +2 -2
- package/src/server/sse-payload-rewrite.ts +20 -3
- package/src/types.ts +10 -1
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +7 -0
- package/src/usage/log.ts +1 -2
- package/vendor/ocx-notch/win32-x64/ocx-notch.exe +0 -0
package/gui/dist/index.html
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-Ch-99jy3.js"></script>
|
|
20
20
|
<link rel="stylesheet" crossorigin href="/assets/index-CxisOo-q.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coseung2/opencodex",
|
|
3
|
-
"version": "2.8.0-cs.
|
|
3
|
+
"version": "2.8.0-cs.17",
|
|
4
4
|
"description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./bin/package-main.mjs",
|
|
@@ -45,7 +45,7 @@ Startup failures and Rust panics are written to the bounded diagnostic log `%LOC
|
|
|
45
45
|
- Online shutdown uses OCX's authenticated `POST /api/stop` graceful-stop endpoint directly, with CLI fallback for older OCX versions. Power transitions probe health immediately and then every ~75ms with a short timeout so the control reflects the real listener state quickly.
|
|
46
46
|
- The header keeps Private and WS on separate rows, with fixed 0-to-Max segmented gauges beside them. The unboxed power and minus controls retain generous invisible hit areas and show hover/pressed feedback.
|
|
47
47
|
- Click the notch to expand provider details.
|
|
48
|
-
- Use the inline Providers and Logs tabs below the memory header to switch content. Logs show only the latest 10 requests with status,
|
|
48
|
+
- Use the inline Providers and Logs tabs below the memory header to switch content. Logs show only the latest 10 requests with status, output tok/s, relative time, reasoning effort, Fast state, and token usage. Estimated rates are prefixed with `~`; unavailable rates use an em dash.
|
|
49
49
|
- When expanded, click the top-right minus control to collapse back to the 58px notch.
|
|
50
50
|
- Drag either side edge to resize the notch width. Position and width are restored on the next launch.
|
|
51
51
|
- Drag anywhere on the notch to move it; its chosen position is preserved while it expands, collapses, or refreshes.
|
|
@@ -58,3 +58,5 @@ Startup failures and Rust panics are written to the bounded diagnostic log `%LOC
|
|
|
58
58
|
- Right-click to add a provider, set the real OCX account rotation threshold, fine-tune it by 1%, **Refresh**, or **Exit**. The opaque provider modal groups supported presets into Account, Free, and Paid tabs. Canonical OpenAI adds another Codex account, OAuth presets use browser/device authorization, and required-key presets use masked API-key entry. Fixed-endpoint key-optional presets (including OpenCode Free and MiMo Free) accept an empty key; entering a key instead adds a distinct account slot to OCX's existing key pool, which can be switched from the provider row. Ollama, vLLM, and LM Studio use local auth without a key. Cloudflare Workers AI also appears in Free and asks for its Account ID before creating the provider. Endpoint-choice and other unresolved placeholder-URL presets remain omitted because this compact modal cannot preserve those setup contracts safely. `Off` writes threshold `0`.
|
|
59
59
|
|
|
60
60
|
Quota percentages are shown as used percentages with 5-hour/weekly/monthly/custom-window columns in one compact row, reset countdowns, 5px progress bars, green fill, and the green-to-amber threshold warning used by the OCX dashboard. Columns are derived from whichever quota windows the management API returns, so provider-specific windows appear without a plan-name list in the notch. Provider usage is merged by exact provider name, limited to the newest day, and formatted with Korean `만/억/조` units. OpenAI account rows show their own 5-hour/weekly/monthly quotas; OAuth and key-pool rows show their masked identity and active/health state, while provider-level quota remains associated with the active account. The native window uses a subtle 238/255 global alpha.
|
|
61
|
+
|
|
62
|
+
For OpenCode Go, the collapsed header keeps the provider report's 5-hour/weekly/monthly windows rather than copying a monthly-only key row, and each expanded key row shows that key's own 5-hour/weekly/monthly allocation.
|
package/src/adapters/base.ts
CHANGED
|
@@ -30,6 +30,13 @@ export interface ProviderAdapter {
|
|
|
30
30
|
*/
|
|
31
31
|
buildRequest(parsed: OcxParsedRequest, incoming: IncomingMeta): AdapterRequest | Promise<AdapterRequest>;
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Decide before request construction that the input already contains the completed answer and
|
|
35
|
+
* nothing should be sent upstream. The server turns this into an outputless completed response
|
|
36
|
+
* and deliberately bypasses empty-completion retry.
|
|
37
|
+
*/
|
|
38
|
+
localTerminal?(parsed: OcxParsedRequest): AdapterLocalTerminal | undefined;
|
|
39
|
+
|
|
33
40
|
fetchResponse?(request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response>;
|
|
34
41
|
|
|
35
42
|
parseStream(response: Response, budget: TranslatorBudget): AsyncGenerator<AdapterEvent>;
|
|
@@ -41,6 +48,11 @@ export interface ProviderAdapter {
|
|
|
41
48
|
): Promise<void>;
|
|
42
49
|
}
|
|
43
50
|
|
|
51
|
+
export interface AdapterLocalTerminal {
|
|
52
|
+
/** Fixed diagnostic identifier; never derived from conversation content. */
|
|
53
|
+
reason: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
44
56
|
export interface AdapterRequest {
|
|
45
57
|
url: string;
|
|
46
58
|
method: string;
|
package/src/adapters/identity.ts
CHANGED
|
@@ -25,7 +25,7 @@ export const CODEX_GPT5_IDENTITY_LINE_AGENT = "You are Codex, an agent based on
|
|
|
25
25
|
* Avoid a broad `You are Codex.*` rewrite that could touch unrelated content.
|
|
26
26
|
*/
|
|
27
27
|
const CODEX_GPT5_IDENTITY_RE =
|
|
28
|
-
/You are Codex, (?:a coding agent|an agent) based on GPT-
|
|
28
|
+
/You are Codex, (?:a coding agent|an agent) based on GPT-[0-9]+(?:\.[0-9]+)*\./g;
|
|
29
29
|
|
|
30
30
|
/** Proxy-neutral replacement: no "opencodex proxy" mention, just the GPT-5/OpenAI disclaimer. */
|
|
31
31
|
export const NEUTRAL_IDENTITY_LINE = "You are a coding agent. Do not claim to be GPT-5 or to be made by OpenAI.";
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-conversation correction for Kiro's heuristic context estimate.
|
|
3
|
+
*
|
|
4
|
+
* Kiro reports `contextUsagePercentage`; against a known context window that is an authoritative
|
|
5
|
+
* post-response checkpoint. We learn a bounded, smoothed charged/estimated factor per conversation
|
|
6
|
+
* and apply it to later turns. State is process-local, bounded, and never persisted.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const MIN_FACTOR = 0.7;
|
|
10
|
+
const MAX_FACTOR = 3;
|
|
11
|
+
const SMOOTHING = 0.35;
|
|
12
|
+
const MAX_TRACKED_CONVERSATIONS = 256;
|
|
13
|
+
const MAX_PLAUSIBLE_OBSERVATION = 6;
|
|
14
|
+
|
|
15
|
+
interface Calibration {
|
|
16
|
+
factor?: number;
|
|
17
|
+
/** Raw heuristic estimate most recently built for this conversation. */
|
|
18
|
+
rawEstimate?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const calibrations = new Map<string, Calibration>();
|
|
22
|
+
|
|
23
|
+
function touch(conversationId: string, update: (current: Calibration) => Calibration): void {
|
|
24
|
+
const current = calibrations.get(conversationId) ?? {};
|
|
25
|
+
calibrations.delete(conversationId);
|
|
26
|
+
calibrations.set(conversationId, update(current));
|
|
27
|
+
while (calibrations.size > MAX_TRACKED_CONVERSATIONS) {
|
|
28
|
+
const oldest = calibrations.keys().next();
|
|
29
|
+
if (oldest.done) break;
|
|
30
|
+
calibrations.delete(oldest.value);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function clamp(value: number): number {
|
|
35
|
+
return Math.min(MAX_FACTOR, Math.max(MIN_FACTOR, value));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Record one terminal turn's measured Kiro input charge. Invalid/implausible readings are ignored. */
|
|
39
|
+
export function recordKiroCalibration(
|
|
40
|
+
conversationId: string | undefined,
|
|
41
|
+
estimated: number,
|
|
42
|
+
charged: number,
|
|
43
|
+
): void {
|
|
44
|
+
if (!conversationId || !Number.isFinite(estimated) || !Number.isFinite(charged)) return;
|
|
45
|
+
if (estimated <= 0 || charged <= 0) return;
|
|
46
|
+
const entry = calibrations.get(conversationId);
|
|
47
|
+
// Learning must use the RAW heuristic, not an already-corrected estimate, or the correction
|
|
48
|
+
// measures its own residual and converges short of the actual provider charge.
|
|
49
|
+
const baseline = entry?.rawEstimate ?? estimated;
|
|
50
|
+
if (!Number.isFinite(baseline) || baseline <= 0) return;
|
|
51
|
+
const observed = charged / baseline;
|
|
52
|
+
if (!Number.isFinite(observed) || observed <= 0 || observed > MAX_PLAUSIBLE_OBSERVATION) return;
|
|
53
|
+
|
|
54
|
+
const previous = entry?.factor ?? 1;
|
|
55
|
+
const next = clamp(previous + (observed - previous) * SMOOTHING);
|
|
56
|
+
// Consume the baseline so duplicate metadata cannot re-score a stale payload.
|
|
57
|
+
touch(conversationId, () => ({ factor: next }));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Apply the learned factor and remember the raw estimate used for the next observation. */
|
|
61
|
+
export function calibrateKiroEstimate(conversationId: string | undefined, estimate: number): number {
|
|
62
|
+
if (!conversationId || !Number.isFinite(estimate) || estimate <= 0) return estimate;
|
|
63
|
+
touch(conversationId, current => ({ ...current, rawEstimate: estimate }));
|
|
64
|
+
const factor = calibrations.get(conversationId)?.factor;
|
|
65
|
+
return factor === undefined ? estimate : Math.ceil(estimate * factor);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Carry calibration to a provider-returned conversation id when it differs from the request id. */
|
|
69
|
+
export function rekeyKiroCalibration(
|
|
70
|
+
fromConversationId: string | undefined,
|
|
71
|
+
toConversationId: string | undefined,
|
|
72
|
+
): void {
|
|
73
|
+
if (!fromConversationId || !toConversationId || fromConversationId === toConversationId) return;
|
|
74
|
+
const entry = calibrations.get(fromConversationId);
|
|
75
|
+
if (!entry) return;
|
|
76
|
+
calibrations.delete(fromConversationId);
|
|
77
|
+
touch(toConversationId, current => ({ ...current, ...entry }));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Test-only reset. */
|
|
81
|
+
export function resetKiroCalibration(): void {
|
|
82
|
+
calibrations.clear();
|
|
83
|
+
}
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
export const KIRO_COMPLETION_TOOL_NAME = "codex_kiro_final_answer";
|
|
2
|
+
|
|
3
|
+
/** Kiro CLI's public service profile for Builder ID requests, NOT an account identity.
|
|
4
|
+
* Request construction only: never persist it or use it to infer account/region ownership.
|
|
5
|
+
* Upstream reference: 0209234e4 + 1241021d8. */
|
|
6
|
+
export const KIRO_BUILDER_ID_SERVICE_PROFILE_ARN =
|
|
7
|
+
"arn:aws:codewhisperer:us-east-1:638616132270:profile/AAAACCCCXXXX";
|
|
2
8
|
export const KIRO_CONTINUATION_MESSAGE =
|
|
3
9
|
"Continue from the prior conversation. Do not quote or mention this instruction.";
|
|
4
10
|
export const KIRO_COMPLETION_RETRY_MESSAGE =
|
|
5
|
-
`Continue the existing task without quoting this instruction. If the task is complete, call ${KIRO_COMPLETION_TOOL_NAME} now with the complete final answer. Otherwise issue the next real tool call now. Do not
|
|
11
|
+
`Continue the existing task without quoting this instruction. If the task is complete, call ${KIRO_COMPLETION_TOOL_NAME} now with the complete final answer. If you cannot continue until the user supplies a decision, information, or a clarification that only they can give, call ${KIRO_COMPLETION_TOOL_NAME} now with that question as the answer. Otherwise issue the next real tool call now. Do not solicit a new task and do not emit another progress-only message.`;
|
|
6
12
|
|
|
7
13
|
export const KIRO_TOOL_RESULT_CARRIER_MESSAGE = "The requested tool result is attached.";
|
|
8
14
|
export const KIRO_EMPTY_TOOL_RESULT_MESSAGE = "The tool completed without textual output.";
|
|
15
|
+
/** Structurally valid trailing user turn for history that already ended in a delivered final answer. */
|
|
16
|
+
export const KIRO_ANSWER_DELIVERED_MESSAGE =
|
|
17
|
+
"The previous final answer was delivered to the user and that task is closed. No new request has been made yet. Do not repeat, revise, or continue that work; wait for the user's next instruction.";
|
|
9
18
|
|
|
10
19
|
export const KIRO_COMPLETION_INSTRUCTIONS =
|
|
11
|
-
`When tools are available, ordinary assistant text is mid-task commentary and does not end the turn. Continue using tools after progress updates, but never repeat or paraphrase an earlier progress update; call the next real tool directly unless a new concise update adds material information. When the task is fully complete and no more tool calls are needed, call ${KIRO_COMPLETION_TOOL_NAME} exactly once with the complete user-facing final answer in \`answer\`. Do not provide the final answer as ordinary assistant text.`;
|
|
20
|
+
`When tools are available, ordinary assistant text is mid-task commentary and does not end the turn. Continue using tools after progress updates, but never repeat or paraphrase an earlier progress update; call the next real tool directly unless a new concise update adds material information. When the task is fully complete and no more tool calls are needed, call ${KIRO_COMPLETION_TOOL_NAME} exactly once with the complete user-facing final answer in \`answer\`. Do not provide the final answer as ordinary assistant text. This completion tool is not an ordinary work tool. When the task is complete, call it instead of emitting answer-shaped ordinary assistant text. The call is terminal and is the exception to generic tool-result counting: it is complete when issued, ends the turn, returns no tool result, and no text or tool call may follow it. If you cannot continue until the user supplies a decision, information, or a clarification that only they can give, that question is your final answer: call ${KIRO_COMPLETION_TOOL_NAME} with the question and stop. Do not write the question as ordinary text and then answer it yourself.`;
|
|
12
21
|
|
|
13
22
|
export type KiroCompletionMode = "disabled" | "required" | "text_fallback";
|
|
14
23
|
|
|
@@ -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":
|
|
@@ -75,6 +75,14 @@ export class KiroThinkingParser {
|
|
|
75
75
|
return [];
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/** Release any partial tag/content carry when the owning stream stops early. */
|
|
79
|
+
dispose(): void {
|
|
80
|
+
this.replaceCarry("preBuffer", "");
|
|
81
|
+
this.replaceCarry("thinkingBuffer", "");
|
|
82
|
+
this.closeTag = "";
|
|
83
|
+
this.state = "streaming";
|
|
84
|
+
}
|
|
85
|
+
|
|
78
86
|
private drainThinking(): AdapterEvent[] {
|
|
79
87
|
const close = this.closeTag;
|
|
80
88
|
const idx = this.thinkingBuffer.indexOf(close);
|
|
@@ -89,8 +97,16 @@ export class KiroThinkingParser {
|
|
|
89
97
|
return events;
|
|
90
98
|
}
|
|
91
99
|
if (this.thinkingBuffer.length <= MAX_CLOSE_TAG) return [];
|
|
92
|
-
|
|
93
|
-
|
|
100
|
+
// Never split a surrogate pair at the send boundary: a lone high
|
|
101
|
+
// surrogate at the end of one delta encodes as U+FFFD. Move the cut one
|
|
102
|
+
// unit earlier so the whole pair stays in the carry.
|
|
103
|
+
let cut = this.thinkingBuffer.length - MAX_CLOSE_TAG;
|
|
104
|
+
if (cut > 0 && cut < this.thinkingBuffer.length) {
|
|
105
|
+
const atCut = this.thinkingBuffer.charCodeAt(cut - 1);
|
|
106
|
+
if (atCut >= 0xd800 && atCut <= 0xdbff) cut -= 1;
|
|
107
|
+
}
|
|
108
|
+
const send = this.thinkingBuffer.slice(0, cut);
|
|
109
|
+
this.replaceCarry("thinkingBuffer", this.thinkingBuffer.slice(cut));
|
|
94
110
|
return send ? [{ type: "reasoning_raw_delta", text: send }] : [];
|
|
95
111
|
}
|
|
96
112
|
}
|
|
@@ -103,7 +103,7 @@ function ensureRootObjectType(schema: unknown): Record<string, unknown> {
|
|
|
103
103
|
// Seed with the root's own properties/required so a schema like
|
|
104
104
|
// { type:"object", properties:{path}, required:["path"], oneOf:[...] } keeps them.
|
|
105
105
|
if (obj.properties && typeof obj.properties === "object") {
|
|
106
|
-
Object.assign(props,
|
|
106
|
+
Object.assign(props, sanitizeSchemaMap(obj.properties) as Record<string, unknown>);
|
|
107
107
|
}
|
|
108
108
|
if (Array.isArray(obj.required)) {
|
|
109
109
|
for (const r of obj.required) if (typeof r === "string") required.add(r);
|
|
@@ -118,7 +118,7 @@ function ensureRootObjectType(schema: unknown): Record<string, unknown> {
|
|
|
118
118
|
if (!variant || typeof variant !== "object" || Array.isArray(variant)) continue;
|
|
119
119
|
const v = variant as Record<string, unknown>;
|
|
120
120
|
if (v.properties && typeof v.properties === "object") {
|
|
121
|
-
Object.assign(props,
|
|
121
|
+
Object.assign(props, sanitizeSchemaMap(v.properties) as Record<string, unknown>);
|
|
122
122
|
}
|
|
123
123
|
if (mergeRequired && Array.isArray(v.required)) {
|
|
124
124
|
for (const r of v.required) if (typeof r === "string") required.add(r);
|
|
@@ -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 {
|