@bitkyc08/opencodex 2.36.0-preview.20260829 → 2.36.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-Cy7Z_pl0.css +1 -0
- package/gui/dist/assets/index-DO8liQVL.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +2 -1
- package/src/AGENTS.md +2 -1
- package/src/adapters/agentrouter.ts +50 -0
- package/src/adapters/anthropic.ts +1 -51
- package/src/adapters/cursor/call-id.ts +76 -8
- package/src/adapters/cursor/checkpoint-store.ts +6 -1
- package/src/adapters/cursor/cursor-errors.ts +44 -0
- package/src/adapters/cursor/native-exec.ts +13 -0
- package/src/adapters/cursor/protobuf-request.ts +651 -29
- package/src/adapters/cursor/tool-result-normalize.ts +3 -3
- package/src/adapters/cursor/transport-retry.ts +5 -1
- package/src/adapters/cursor.ts +15 -1
- package/src/adapters/empty-tool-output-annotation.ts +43 -0
- package/src/adapters/exec-tool-result-normalize.ts +70 -5
- package/src/adapters/google.ts +22 -2
- package/src/adapters/kiro.ts +26 -2
- package/src/adapters/ollama-native-url.ts +111 -0
- package/src/adapters/ollama-native.ts +1131 -0
- package/src/adapters/openai-chat.ts +30 -7
- package/src/adapters/openai-responses.ts +72 -4
- package/src/adapters/registry.ts +7 -0
- package/src/adapters/xai-web-search.ts +58 -0
- package/src/claude/desktop-3p.ts +21 -1
- package/src/claude/desktop-policy.ts +149 -0
- package/src/cli/account.ts +16 -2
- package/src/cli/claude-desktop.ts +13 -3
- package/src/cli/combo.ts +8 -5
- package/src/cli/doctor.ts +77 -11
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +16 -0
- package/src/cli/models.ts +20 -3
- package/src/cli/registry.ts +2 -1
- package/src/cli/status.ts +140 -2
- package/src/cli/storage.ts +10 -1
- package/src/codex/account-runtime-state.ts +39 -5
- package/src/codex/account-store.ts +393 -13
- package/src/codex/account-usability.ts +11 -4
- package/src/codex/app-server-processes.ts +46 -5
- package/src/codex/auth-context.ts +160 -32
- package/src/codex/catalog/bundled.ts +7 -5
- package/src/codex/catalog/metadata.ts +1 -1
- package/src/codex/catalog/parsing.ts +57 -1
- package/src/codex/catalog/provider-fetch.ts +61 -4
- package/src/codex/catalog/sync.ts +4 -3
- package/src/codex/convergence.ts +3 -2
- package/src/codex/data/upstream-models.json +40 -8
- package/src/codex/inject-coordination.ts +111 -14
- package/src/codex/integration-record.ts +12 -2
- package/src/codex/main-account.ts +225 -1
- package/src/codex/model-entitlements.ts +339 -27
- package/src/codex/prompt-layers.ts +346 -7
- package/src/codex/prompt-text-probe.ts +272 -21
- package/src/codex/routing.ts +693 -132
- package/src/codex/runtime.ts +12 -0
- package/src/codex/subagent-model-fallback.ts +62 -24
- package/src/codex/user-identity.ts +33 -25
- package/src/combos/index.ts +1 -0
- package/src/combos/reset-window.ts +46 -0
- package/src/combos/resolve.ts +84 -2
- package/src/combos/types.ts +5 -2
- package/src/config/atomic-write.ts +104 -22
- package/src/config/provider-validation.ts +11 -0
- package/src/config.ts +75 -3
- package/src/generated/compatibility-version.json +207 -131
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/catalog.ts +71 -0
- package/src/grok/effort.ts +83 -0
- package/src/grok/inject.ts +952 -127
- package/src/grok/models.ts +56 -0
- package/src/grok/status.ts +21 -8
- package/src/grok/sync.ts +10 -18
- package/src/images/loop.ts +6 -3
- package/src/integrations/native/ownership-preflight.ts +4 -1
- package/src/lab/fabric/producer-isolate.ts +36 -3
- package/src/lib/destination-policy.ts +93 -7
- package/src/lib/redact.ts +6 -1
- package/src/lib/shadow-call.ts +38 -3
- package/src/lib/test-home-guard.ts +18 -3
- package/src/lib/upstream-retry.ts +43 -6
- package/src/lib/windows-secret-acl.ts +66 -0
- package/src/lib/windows-text.ts +28 -2
- package/src/lib/windows-user-principal.ts +35 -23
- package/src/oauth/account-quota-rank.ts +107 -0
- package/src/oauth/anthropic-routing.ts +125 -30
- package/src/oauth/chatgpt.ts +5 -1
- package/src/oauth/generic-account-failover.ts +114 -7
- package/src/oauth/index.ts +15 -8
- package/src/oauth/store.ts +16 -0
- package/src/providers/account-quota-disk.ts +79 -0
- package/src/providers/command-code-efforts.ts +24 -0
- package/src/providers/derive.ts +6 -0
- package/src/providers/key-failover.ts +33 -1
- package/src/providers/kiro-usage.ts +272 -0
- package/src/providers/ollama-show.ts +311 -0
- package/src/providers/openai-sidecar.ts +5 -0
- package/src/providers/quota-routing-cache.ts +32 -0
- package/src/providers/quota-types.ts +36 -0
- package/src/providers/quota-wire.ts +102 -0
- package/src/providers/quota.ts +208 -147
- package/src/providers/registry.ts +68 -8
- package/src/providers/slug-codec.ts +12 -4
- package/src/providers/vercel-gateway-routing.ts +108 -0
- package/src/router.ts +22 -12
- package/src/server/auth-cors.ts +26 -0
- package/src/server/catalog-download.ts +73 -0
- package/src/server/chat-native.ts +12 -2
- package/src/server/gui-static.ts +4 -1
- package/src/server/index.ts +132 -9
- package/src/server/management/agent-settings-routes.ts +38 -5
- package/src/server/management/codex-prompt-routes.ts +7 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/config-routes.ts +9 -1
- package/src/server/management/context.ts +5 -0
- package/src/server/management/model-routes.ts +16 -6
- package/src/server/management/native-integration-routes.ts +12 -17
- package/src/server/management/oauth-account-routes.ts +13 -0
- package/src/server/management/provider-routes.ts +32 -5
- package/src/server/management/routing-profile-routes.ts +15 -0
- package/src/server/management/shadow-call-validation.ts +29 -0
- package/src/server/management-api.ts +7 -3
- package/src/server/request-log.ts +3 -5
- package/src/server/responses/agent-task-recovery-cache.ts +8 -0
- package/src/server/responses/agent-task-recovery.ts +52 -20
- package/src/server/responses/codex-auth-error.ts +26 -0
- package/src/server/responses/compact.ts +345 -10
- package/src/server/responses/core.ts +736 -108
- package/src/server/responses/empty-completion-guard.ts +16 -0
- package/src/server/responses/fetch-helpers.ts +42 -0
- package/src/server/responses/policy-fallback.ts +11 -6
- package/src/server/responses-undeclared-tool-guard.ts +16 -3
- package/src/server/startup-health-cache.ts +59 -13
- package/src/service-manager-probe.ts +115 -9
- package/src/service.ts +139 -40
- package/src/storage/cleanup.ts +10 -0
- package/src/storage/storage-mutation-coordinator.ts +14 -3
- package/src/tray/windows-tray.ps1 +10 -4
- package/src/tray/windows.ts +30 -2
- package/src/types/config.ts +27 -14
- package/src/types/provider.ts +54 -0
- package/src/types/tools.ts +13 -3
- package/src/types.ts +4 -0
- package/src/usage/summary.ts +421 -177
- package/src/vision/anthropic-describe.ts +3 -3
- package/src/vision/describe.ts +5 -3
- package/src/web-search/anthropic-executor.ts +9 -2
- package/src/web-search/exa-executor.ts +3 -3
- package/src/web-search/executor.ts +8 -3
- package/src/web-search/gemini-executor.ts +3 -3
- package/src/web-search/loop.ts +11 -3
- package/src/web-search/xai-executor.ts +3 -3
- package/gui/dist/assets/index-DQ-Ie18T.css +0 -1
- package/gui/dist/assets/index-DX0RGj3e.js +0 -112
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
EMPTY_EXEC_OUTPUT_MESSAGE,
|
|
14
14
|
EMPTY_EXEC_OUTPUT_REGEX,
|
|
15
15
|
FAILED_EXEC_OUTPUT_MESSAGE,
|
|
16
|
-
|
|
16
|
+
isFailedEmptyExecWrapper,
|
|
17
17
|
isCodexExecBridgeTool,
|
|
18
18
|
} from "../exec-tool-result-normalize";
|
|
19
19
|
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
* `Script failed`, so restore that arm here rather than widening the shared one.
|
|
24
24
|
*/
|
|
25
25
|
function isEmptyOrFailedExecWrapper(text: string): boolean {
|
|
26
|
-
return EMPTY_EXEC_OUTPUT_REGEX.test(text) ||
|
|
26
|
+
return EMPTY_EXEC_OUTPUT_REGEX.test(text) || isFailedEmptyExecWrapper(text);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const COMPUTER_USE_TOOL_NAMES = new Set([
|
|
@@ -99,7 +99,7 @@ export function normalizeCursorToolResultText(
|
|
|
99
99
|
// A `Script failed` wrapper is empty but NOT a success: reporting it as an empty success
|
|
100
100
|
// would erase the only failure signal. Text classification stays separate from Cursor's
|
|
101
101
|
// isError policy, which the Computer Use branch above owns.
|
|
102
|
-
text:
|
|
102
|
+
text: isFailedEmptyExecWrapper(text.trim()) ? FAILED_EXEC_OUTPUT_MESSAGE : EMPTY_EXEC_OUTPUT_MESSAGE,
|
|
103
103
|
isError: false,
|
|
104
104
|
changed: true,
|
|
105
105
|
};
|
|
@@ -2,7 +2,7 @@ import type { CursorRunRequest, CursorServerMessage } from "./types";
|
|
|
2
2
|
import type { CursorTransport, CursorTransportFactory, CursorTransportFactoryInput } from "./transport";
|
|
3
3
|
import { abortError, retryBackoffDelayMs, sleepWithAbort } from "../../lib/upstream-retry";
|
|
4
4
|
import { debugProviderDiagnostic } from "../../lib/debug";
|
|
5
|
-
import { safeCursorErrorMessage } from "./cursor-errors";
|
|
5
|
+
import { isCursorRootEnvelopeError, safeCursorErrorMessage } from "./cursor-errors";
|
|
6
6
|
|
|
7
7
|
// Compat: historical name for the shared abortable sleep, kept for external callers.
|
|
8
8
|
export { sleepWithAbort as abortAwareSleep } from "../../lib/upstream-retry";
|
|
@@ -18,6 +18,10 @@ export const CURSOR_RETRY_MAX_MS = 2_000;
|
|
|
18
18
|
* never replay a turn the Cursor server might already have accepted.
|
|
19
19
|
*/
|
|
20
20
|
export function isRetryableCursorError(err: unknown): boolean {
|
|
21
|
+
// Local envelope failures are deterministic: the same request produces the same rejection.
|
|
22
|
+
// Checked before the text heuristics below, which would otherwise have to infer this from
|
|
23
|
+
// wording.
|
|
24
|
+
if (isCursorRootEnvelopeError(err)) return false;
|
|
21
25
|
const code = typeof err === "object" && err && "code" in err ? String((err as { code?: unknown }).code ?? "") : "";
|
|
22
26
|
const message = err instanceof Error ? err.message : typeof err === "string" ? err : "";
|
|
23
27
|
const haystack = `${code} ${message}`.toLowerCase();
|
package/src/adapters/cursor.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { AdapterEvent, OcxProviderConfig } from "../types";
|
|
|
3
3
|
import type { ProviderAdapter } from "./base";
|
|
4
4
|
import { isTranslatorBudgetExceededError } from "../lib/translator-budget";
|
|
5
5
|
import { cursorExecDeniedMessage, cursorRequestDeclaresFullAccess } from "./cursor/exec-policy";
|
|
6
|
-
import { isCursorBenignCancelError, isCursorInvalidArgumentError, safeCursorErrorMessage, type CursorSizeContext } from "./cursor/cursor-errors";
|
|
6
|
+
import { isCursorBenignCancelError, isCursorInvalidArgumentError, isCursorRootEnvelopeError, safeCursorErrorMessage, type CursorSizeContext } from "./cursor/cursor-errors";
|
|
7
7
|
import { cursorCheckpointModelAffinityId, inferCursorContextWindow, isCursorExternalWireModel } from "./cursor/discovery";
|
|
8
8
|
import { createCursorKvStore, type CursorKvStore } from "./cursor/kv-store";
|
|
9
9
|
import { mapCursorServerMessage } from "./cursor/message-mapper";
|
|
@@ -70,6 +70,14 @@ function safeCursorTransportError(err: unknown, sizeContext?: CursorSizeContext)
|
|
|
70
70
|
if (err instanceof CursorMissingCredentialError) {
|
|
71
71
|
return "Cursor live transport is enabled, but no Cursor access token is configured. Set provider.apiKey or OPENCODEX_CURSOR_TEST_TOKEN.";
|
|
72
72
|
}
|
|
73
|
+
// A locally raised envelope rejection is already safe, specific, and actionable: it was composed
|
|
74
|
+
// here from our own measurements and contains no upstream text. Passing it through
|
|
75
|
+
// `safeCursorErrorMessage` would collapse it to the bare label "Cursor invalid request" (it
|
|
76
|
+
// matches the "invalid"/"exceeds" keyword branch) and discard the counts that tell the operator
|
|
77
|
+
// which limit was hit and by how much.
|
|
78
|
+
if (isCursorRootEnvelopeError(err)) {
|
|
79
|
+
return err instanceof Error ? `Cursor invalid request: ${err.message}` : "Cursor invalid request";
|
|
80
|
+
}
|
|
73
81
|
const message = err instanceof Error ? err.message : typeof err === "string" ? err : undefined;
|
|
74
82
|
if (message) return safeCursorErrorMessage(message, sizeContext);
|
|
75
83
|
return "Cursor upstream error: transport failed before completion.";
|
|
@@ -485,6 +493,12 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
485
493
|
...(isTranslatorBudgetExceededError(err)
|
|
486
494
|
? { status: 502, errorType: "upstream_error", code: "translation_buffer_limit" }
|
|
487
495
|
: {}),
|
|
496
|
+
// A local envelope rejection is a client error with a stable code, and the caller needs
|
|
497
|
+
// that code to distinguish "this conversation cannot be sent" from a transient upstream
|
|
498
|
+
// fault. Without this the class was flattened to a bare message and the code was lost.
|
|
499
|
+
...(isCursorRootEnvelopeError(err)
|
|
500
|
+
? { status: 400, errorType: "invalid_request_error", code: "cursor_root_envelope_limit", retryable: false }
|
|
501
|
+
: {}),
|
|
488
502
|
...(partialUsage ? { usage: partialUsage } : {}),
|
|
489
503
|
});
|
|
490
504
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared wire text and emptiness contract for present-but-empty tool outputs.
|
|
3
|
+
*
|
|
4
|
+
* Both the OpenAI Chat and Responses adapters use this module so the two wires
|
|
5
|
+
* cannot drift again: only a pure text/refusal part array whose joined content
|
|
6
|
+
* trims empty is "present but empty". Image, file, encrypted-content and any
|
|
7
|
+
* other non-text part is real output and is never replaced by the annotation.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Wire text used when a present-but-empty tool output must stay visible to the model. */
|
|
11
|
+
export const EMPTY_TOOL_OUTPUT_ANNOTATION =
|
|
12
|
+
"[ocx] empty tool output: the tool ran but produced no stdout or return value; do not treat this as success, failure, or user-provided input.";
|
|
13
|
+
|
|
14
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
15
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Part types that carry wire text on either adapter (Chat uses `text`, Responses uses `input_text`/`output_text`). */
|
|
19
|
+
const TEXT_PART_TYPES = new Set(["text", "input_text", "output_text"]);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* True when every part is text/refusal and the joined text/refusal content trims
|
|
23
|
+
* empty. An empty array is the array twin of a blank string. Any image, file,
|
|
24
|
+
* encrypted-content or other non-text part makes the array non-empty so the
|
|
25
|
+
* model still receives the real payload.
|
|
26
|
+
*/
|
|
27
|
+
export function isWhitespaceOnlyTextPartArray(parts: readonly unknown[]): boolean {
|
|
28
|
+
if (parts.length === 0) return true;
|
|
29
|
+
let joined = "";
|
|
30
|
+
for (const part of parts) {
|
|
31
|
+
if (!isPlainObject(part)) return false;
|
|
32
|
+
if (typeof part.type === "string" && TEXT_PART_TYPES.has(part.type) && typeof part.text === "string") {
|
|
33
|
+
joined += part.text;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (part.type === "refusal" && typeof part.refusal === "string") {
|
|
37
|
+
joined += part.refusal;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
return joined.trim() === "";
|
|
43
|
+
}
|
|
@@ -17,13 +17,78 @@
|
|
|
17
17
|
* `Script failed` is deliberately NOT in this set. A failed cell with no captured output is still
|
|
18
18
|
* a FAILURE, and the success guidance below ("not a blocked tool", "do not re-run") would erase the
|
|
19
19
|
* only signal that anything went wrong — reachable through Responses history, where
|
|
20
|
-
* `function_call_output` is parsed with `isError: false`. Cursor
|
|
21
|
-
* Computer Use, where a failed wrapper is separately marked
|
|
20
|
+
* `function_call_output` is parsed with `isError: false`. Cursor combines this set with
|
|
21
|
+
* `isFailedEmptyExecWrapper` below for Computer Use, where a failed wrapper is separately marked
|
|
22
|
+
* `isError`.
|
|
22
23
|
*/
|
|
23
24
|
export const EMPTY_EXEC_OUTPUT_REGEX = /^(?:(?:Script completed|Command finished|Execution finished)[^\n]*\n+)?(?:Wall time[^\n]*\n+)?(?:Output:\s*)?(?:<empty>)?\s*$/;
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
function skipFailedWrapperBlankSeparators(text: string, start: number): number {
|
|
27
|
+
let index = start;
|
|
28
|
+
while (index < text.length) {
|
|
29
|
+
if (text[index] === "\n") {
|
|
30
|
+
index += 1;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
// A CRLF blank line is one separator, not a stray carriage return. The regex this replaced
|
|
34
|
+
// matched only `\n`, so a Windows-produced wrapper never classified and the failure guidance
|
|
35
|
+
// was silently replaced by the empty-SUCCESS message on that platform.
|
|
36
|
+
if (text[index] === "\r" && text[index + 1] === "\n") {
|
|
37
|
+
index += 2;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
return index;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function skipFailedWrapperWhitespace(text: string, start: number): number {
|
|
46
|
+
let index = start;
|
|
47
|
+
while (index < text.length && text[index]!.trim() === "") index += 1;
|
|
48
|
+
return index;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function skipFailedWrapperLine(text: string, start: number): number {
|
|
52
|
+
const newline = text.indexOf("\n", start);
|
|
53
|
+
return newline === -1 ? text.length : skipFailedWrapperBlankSeparators(text, newline + 1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Wrapper for a cell that FAILED without emitting output: empty, but not a success.
|
|
58
|
+
*
|
|
59
|
+
* A single forward scan, replacing a regex whose `\n*` and `\s*` runs sat adjacent over the same
|
|
60
|
+
* span and could be made to backtrack on a long whitespace run followed by one non-matching
|
|
61
|
+
* character. Every loop here advances an index monotonically and the token checks are fixed-length,
|
|
62
|
+
* so the work is bounded by the input length with no path that retries a prefix.
|
|
63
|
+
*
|
|
64
|
+
* Two boundaries are load-bearing and both were divergences in an earlier attempt at this rewrite:
|
|
65
|
+
*
|
|
66
|
+
* - Whitespace after `Output:` may precede the marker, so an INDENTED `<empty>` still classifies.
|
|
67
|
+
* Rejecting it would leave the wrapper unnormalized and the failure unexplained.
|
|
68
|
+
* - Only whitespace may follow the marker, so a DUPLICATE `<empty>` still does not classify.
|
|
69
|
+
* Accepting it would erase a real payload as an empty failed wrapper — the damaging direction.
|
|
70
|
+
*
|
|
71
|
+
* Behaviour is otherwise identical to the regex; the CRLF separators above are the only
|
|
72
|
+
* intentional change, verified against a 63-shape differential corpus.
|
|
73
|
+
*/
|
|
74
|
+
export function isFailedEmptyExecWrapper(trimmed: string): boolean {
|
|
75
|
+
if (!trimmed.startsWith("Script failed")) return false;
|
|
76
|
+
|
|
77
|
+
const firstNewline = trimmed.indexOf("\n", "Script failed".length);
|
|
78
|
+
if (firstNewline === -1) return true;
|
|
79
|
+
|
|
80
|
+
let index = skipFailedWrapperBlankSeparators(trimmed, firstNewline + 1);
|
|
81
|
+
if (trimmed.startsWith("Wall time", index)) {
|
|
82
|
+
index = skipFailedWrapperLine(trimmed, index);
|
|
83
|
+
}
|
|
84
|
+
if (trimmed.startsWith("Output:", index)) {
|
|
85
|
+
index = skipFailedWrapperWhitespace(trimmed, index + "Output:".length);
|
|
86
|
+
}
|
|
87
|
+
if (trimmed.startsWith("<empty>", index)) {
|
|
88
|
+
index += "<empty>".length;
|
|
89
|
+
}
|
|
90
|
+
return skipFailedWrapperWhitespace(trimmed, index) === trimmed.length;
|
|
91
|
+
}
|
|
27
92
|
|
|
28
93
|
/** Guidance for a failed cell whose output was empty: the failure must survive normalization. */
|
|
29
94
|
export const FAILED_EXEC_OUTPUT_MESSAGE =
|
|
@@ -94,6 +159,6 @@ export function normalizeEmptyExecToolResultText(
|
|
|
94
159
|
if (!isCodexExecBridgeTool(options.toolName, options.toolNamespace)) return undefined;
|
|
95
160
|
const trimmed = text.trim();
|
|
96
161
|
// Failure first: a failed wrapper must never be described as an empty success.
|
|
97
|
-
if (
|
|
162
|
+
if (isFailedEmptyExecWrapper(trimmed)) return FAILED_EXEC_OUTPUT_MESSAGE;
|
|
98
163
|
return EMPTY_EXEC_OUTPUT_REGEX.test(trimmed) ? EMPTY_EXEC_OUTPUT_MESSAGE : undefined;
|
|
99
164
|
}
|
package/src/adapters/google.ts
CHANGED
|
@@ -54,6 +54,16 @@ const GOOGLE_BREVITY_INSTRUCTION = [
|
|
|
54
54
|
"- This applies only to intermediate progress text. Your final answer after the work is done is exempt: write it in full and at whatever length the task requires.",
|
|
55
55
|
].join("\n");
|
|
56
56
|
|
|
57
|
+
const ANTIGRAVITY_REJECTED_CLAUDE_SDK_PARAGRAPH =
|
|
58
|
+
"You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
59
|
+
|
|
60
|
+
function stripAntigravityRejectedClaudeSdkParagraph(systemText: string): string {
|
|
61
|
+
return systemText
|
|
62
|
+
.split("\n\n")
|
|
63
|
+
.filter(paragraph => paragraph !== ANTIGRAVITY_REJECTED_CLAUDE_SDK_PARAGRAPH)
|
|
64
|
+
.join("\n\n");
|
|
65
|
+
}
|
|
66
|
+
|
|
57
67
|
/**
|
|
58
68
|
* Documented output ceiling for a Google-surface model, or `undefined` when the id is not
|
|
59
69
|
* recognized.
|
|
@@ -227,15 +237,19 @@ function geminiOrphanToolResultParts(msg: OcxToolResultMessage): unknown[] {
|
|
|
227
237
|
function messagesToGeminiFormat(
|
|
228
238
|
parsed: OcxParsedRequest,
|
|
229
239
|
identityModelId: string,
|
|
240
|
+
stripRejectedClaudeSdkParagraph = false,
|
|
230
241
|
): { systemInstruction?: unknown; contents: unknown[]; replayedCallIds: string[] } {
|
|
231
242
|
// Neutralize Codex's GPT-5 identity line (Gemini/Antigravity share this path) so a routed model
|
|
232
243
|
// never misreports as GPT-5/OpenAI, and never leaks the proxy identity upstream.
|
|
233
244
|
const toolCatalogNudge = buildNonOpenAIToolCatalogNudgeForTools(parsed.context.tools, parsed.options.toolChoice);
|
|
234
|
-
const
|
|
245
|
+
const identifiedSystemText = identifyRoutedModel([
|
|
235
246
|
...(parsed.context.systemPrompt ?? []),
|
|
236
247
|
...(toolCatalogNudge ? [toolCatalogNudge] : []),
|
|
237
248
|
GOOGLE_BREVITY_INSTRUCTION,
|
|
238
249
|
].join("\n\n"), identityModelId);
|
|
250
|
+
const systemText = stripRejectedClaudeSdkParagraph
|
|
251
|
+
? stripAntigravityRejectedClaudeSdkParagraph(identifiedSystemText)
|
|
252
|
+
: identifiedSystemText;
|
|
239
253
|
const systemInstruction = { parts: [{ text: systemText }] };
|
|
240
254
|
|
|
241
255
|
const contents: unknown[] = [];
|
|
@@ -733,7 +747,13 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
733
747
|
: resolveDirectGeminiWireModelId(parsed.modelId, provider.directGeminiWireRenames !== false);
|
|
734
748
|
// AI Studio's `-tiered` spelling is wire-only; CCA aliases may migrate to another generation.
|
|
735
749
|
const identityModelId = provider.googleMode === "cloud-code-assist" ? routedModelId : parsed.modelId;
|
|
736
|
-
const
|
|
750
|
+
const stripRejectedClaudeSdkParagraph = provider.googleMode === "cloud-code-assist"
|
|
751
|
+
&& parsed.modelId === "gemini-3.7-flash";
|
|
752
|
+
const { systemInstruction, contents, replayedCallIds } = messagesToGeminiFormat(
|
|
753
|
+
parsed,
|
|
754
|
+
identityModelId,
|
|
755
|
+
stripRejectedClaudeSdkParagraph,
|
|
756
|
+
);
|
|
737
757
|
lastInjectedCallIds = [...replayedCallIds];
|
|
738
758
|
lastReasoningReplayScope = parsed._reasoningReplayScope;
|
|
739
759
|
const tools = toolsToGeminiFormat(parsed);
|
package/src/adapters/kiro.ts
CHANGED
|
@@ -1062,6 +1062,27 @@ async function* parseKiroAttemptEvents(
|
|
|
1062
1062
|
try { yield event; } finally { retention.releaseEvent(event); }
|
|
1063
1063
|
}
|
|
1064
1064
|
};
|
|
1065
|
+
// A valid private completion answer supersedes the progress prose staged during the SAME
|
|
1066
|
+
// inference: Kiro emits answer-like text and then calls the completion tool, so releasing both
|
|
1067
|
+
// makes the bridge close the commentary message and open a second one with near-identical text
|
|
1068
|
+
// (#2819 follow-up). Consume the collection instead — drop the redundant text, keep every
|
|
1069
|
+
// non-text event, and release retention either way.
|
|
1070
|
+
//
|
|
1071
|
+
// This is deliberately the ONLY suppression site. The outer drain in `parseKiroAttempt` is also
|
|
1072
|
+
// the leftover flush for early terminal returns (stream, protocol, and provider failures), so
|
|
1073
|
+
// teaching it to discard text would hide the only commentary a failed turn ever produced.
|
|
1074
|
+
// Splicing here leaves that drain empty on the completion path and untouched everywhere else.
|
|
1075
|
+
const consumeSupersededByCompletion = async function* (
|
|
1076
|
+
events: AdapterEvent[],
|
|
1077
|
+
): AsyncGenerator<AdapterEvent> {
|
|
1078
|
+
for (const event of events.splice(0)) {
|
|
1079
|
+
try {
|
|
1080
|
+
if (event.type !== "text_delta") yield event;
|
|
1081
|
+
} finally {
|
|
1082
|
+
retention.releaseEvent(event);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1065
1086
|
|
|
1066
1087
|
const providerState = (): { kiro: { conversationId: string } } | undefined =>
|
|
1067
1088
|
returnedConversationId ? { kiro: { conversationId: returnedConversationId } } : undefined;
|
|
@@ -1466,12 +1487,15 @@ async function* parseKiroAttemptEvents(
|
|
|
1466
1487
|
});
|
|
1467
1488
|
|
|
1468
1489
|
if (mode === "required") {
|
|
1469
|
-
|
|
1490
|
+
// A valid completion answer makes this inference's staged prose redundant; anything else
|
|
1491
|
+
// still flushes exactly as before (bounded fallback, explicit stops, real tool calls).
|
|
1492
|
+
if (completionAnswer !== undefined) yield* consumeSupersededByCompletion(deferred);
|
|
1493
|
+
else yield* emitRetained(deferred.splice(0));
|
|
1470
1494
|
}
|
|
1471
1495
|
|
|
1472
1496
|
if (mode === "text_fallback") {
|
|
1473
1497
|
if (completionAnswer !== undefined) {
|
|
1474
|
-
yield*
|
|
1498
|
+
yield* consumeSupersededByCompletion(fallbackEvents);
|
|
1475
1499
|
yield { type: "text_delta", text: completionAnswer, phase: "final_answer" };
|
|
1476
1500
|
return {
|
|
1477
1501
|
assistantText,
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL policy for Ollama's native REST API.
|
|
3
|
+
*
|
|
4
|
+
* The built-in Ollama provider historically stored an OpenAI-compatible `/v1` base URL. The
|
|
5
|
+
* native adapter deliberately canonicalizes that compatibility spelling only for Ollama's known
|
|
6
|
+
* local/cloud hosts. An arbitrary custom host with a `/v1` path is never silently rewritten.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type OllamaNativeEndpointKind = "local" | "cloud" | "custom";
|
|
10
|
+
|
|
11
|
+
const LOCAL_HOSTNAMES = new Set(["localhost", "127.0.0.1", "::1"]);
|
|
12
|
+
const CLOUD_HOSTNAMES = new Set(["ollama.com"]);
|
|
13
|
+
const REJECTED_CLOUD_ALIAS_HOSTNAMES = new Set(["www.ollama.com"]);
|
|
14
|
+
|
|
15
|
+
function normalizedHostname(url: URL): string {
|
|
16
|
+
const hostname = url.hostname.toLowerCase().replace(/^\[|\]$/gu, "");
|
|
17
|
+
return hostname.endsWith(".") ? hostname.slice(0, -1) : hostname;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizedPath(url: URL): string {
|
|
21
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
22
|
+
return path === "/" ? "" : path;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function endpointKind(url: URL): OllamaNativeEndpointKind {
|
|
26
|
+
// WHATWG URL keeps IPv6 brackets in `hostname` on Bun/Node (`[::1]`), while the
|
|
27
|
+
// loopback policy is stored in its canonical host form (`::1`).
|
|
28
|
+
const hostname = normalizedHostname(url);
|
|
29
|
+
if (REJECTED_CLOUD_ALIAS_HOSTNAMES.has(hostname)) {
|
|
30
|
+
throw new Error("ollama-native requires canonical Ollama Cloud host ollama.com; www.ollama.com is rejected");
|
|
31
|
+
}
|
|
32
|
+
if (LOCAL_HOSTNAMES.has(hostname)) return "local";
|
|
33
|
+
if (CLOUD_HOSTNAMES.has(hostname)) return "cloud";
|
|
34
|
+
return "custom";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function assertCloudTransport(url: URL, kind: OllamaNativeEndpointKind): void {
|
|
38
|
+
if (kind !== "cloud") return;
|
|
39
|
+
if (url.protocol !== "https:") {
|
|
40
|
+
throw new Error("ollama-native canonical Ollama Cloud requires HTTPS");
|
|
41
|
+
}
|
|
42
|
+
if (url.port) {
|
|
43
|
+
throw new Error("ollama-native canonical Ollama Cloud rejects non-default ports");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseBaseUrl(baseUrl: string): URL {
|
|
48
|
+
const trimmed = baseUrl.trim();
|
|
49
|
+
if (!trimmed) throw new Error("ollama-native requires a non-empty baseUrl");
|
|
50
|
+
|
|
51
|
+
let url: URL;
|
|
52
|
+
try {
|
|
53
|
+
url = new URL(trimmed);
|
|
54
|
+
} catch {
|
|
55
|
+
throw new Error("ollama-native requires an absolute http(s) baseUrl");
|
|
56
|
+
}
|
|
57
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
58
|
+
throw new Error("ollama-native only supports http(s) base URLs");
|
|
59
|
+
}
|
|
60
|
+
if (url.username || url.password || url.search || url.hash) {
|
|
61
|
+
throw new Error("ollama-native baseUrl must not contain credentials, a query, or a fragment");
|
|
62
|
+
}
|
|
63
|
+
return url;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Return the endpoint family used by native authentication policy. */
|
|
67
|
+
export function ollamaNativeEndpointKind(baseUrl: string): OllamaNativeEndpointKind {
|
|
68
|
+
const url = parseBaseUrl(baseUrl);
|
|
69
|
+
const kind = endpointKind(url);
|
|
70
|
+
assertCloudTransport(url, kind);
|
|
71
|
+
return kind;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** True when the base URL points at canonical Ollama Cloud (not a self-hosted destination). */
|
|
75
|
+
export function isCanonicalOllamaCloudUrl(baseUrl: string): boolean {
|
|
76
|
+
return ollamaNativeEndpointKind(baseUrl) === "cloud";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Build the native chat endpoint from a configured base URL.
|
|
81
|
+
*
|
|
82
|
+
* Recognized compatibility forms on canonical Ollama hosts:
|
|
83
|
+
* - `/`, `/api`, `/api/chat`
|
|
84
|
+
* - legacy `/v1` and `/v1/chat/completions`
|
|
85
|
+
*
|
|
86
|
+
* For an unrelated custom host only `/`, `/api`, and `/api/chat` are accepted. In particular,
|
|
87
|
+
* `/v1` is rejected instead of being stripped or guessed at.
|
|
88
|
+
*/
|
|
89
|
+
export function ollamaNativeChatUrl(baseUrl: string): string {
|
|
90
|
+
const url = parseBaseUrl(baseUrl);
|
|
91
|
+
const kind = endpointKind(url);
|
|
92
|
+
assertCloudTransport(url, kind);
|
|
93
|
+
const path = normalizedPath(url);
|
|
94
|
+
const canonicalPaths = new Set(["", "/api", "/api/chat", "/v1", "/v1/chat/completions"]);
|
|
95
|
+
const customPaths = new Set(["", "/api", "/api/chat"]);
|
|
96
|
+
|
|
97
|
+
if (kind === "custom" && !customPaths.has(path)) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`ollama-native refuses custom baseUrl path "${path || "/"}"; use a native root, /api, or /api/chat`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
if (kind !== "custom" && !canonicalPaths.has(path)) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`ollama-native refuses Ollama baseUrl path "${path || "/"}"; use root, /v1, /api, or /api/chat`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (kind === "cloud") url.hostname = normalizedHostname(url);
|
|
109
|
+
url.pathname = "/api/chat";
|
|
110
|
+
return url.toString();
|
|
111
|
+
}
|