@bitkyc08/opencodex 2.87.0 → 2.89.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/README.md +11 -1
- package/bin/ocx.mjs +15 -4
- package/gui/dist/assets/App-BnRSujOq.js +58 -0
- package/gui/dist/assets/App-dnkJ85te.css +1 -0
- package/gui/dist/assets/{Tray-DCd-A1az.js → Tray-7z1COnmz.js} +1 -1
- package/gui/dist/assets/index-uchD-ngf.js +94 -0
- package/gui/dist/assets/{quota-summary-ChvmKGOk.js → quota-summary-BY_10IEz.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic/cache-retention.ts +117 -0
- package/src/adapters/anthropic/passthrough.ts +7 -4
- package/src/adapters/anthropic-rejection-memo.ts +71 -0
- package/src/adapters/anthropic-replay.ts +135 -0
- package/src/adapters/anthropic-tool-catalog.ts +11 -0
- package/src/adapters/anthropic.ts +86 -48
- package/src/adapters/coding-agent/protocol.ts +6 -0
- package/src/adapters/google-http.ts +3 -2
- package/src/adapters/google-progress-history.ts +53 -0
- package/src/adapters/google-vertex-endpoint.ts +67 -0
- package/src/adapters/google.ts +52 -30
- package/src/adapters/openai-responses/compaction-search-history.ts +27 -0
- package/src/adapters/openai-responses/passthrough.ts +17 -12
- package/src/adapters/openai-responses/reasoning.ts +68 -1
- package/src/adapters/registry.ts +1 -1
- package/src/adapters/upstream-http-error.ts +3 -2
- package/src/adapters/zed.ts +7 -4
- package/src/bridge/internal.ts +17 -3
- package/src/bridge/response-json.ts +4 -4
- package/src/bridge/sse.ts +17 -10
- package/src/claude/inbound-content-options.ts +66 -0
- package/src/claude/inbound.ts +12 -26
- package/src/claude/outbound.ts +18 -5
- package/src/cli/account-api.ts +13 -1
- package/src/cli/account-extended.ts +17 -5
- package/src/cli/account-key-quota.ts +4 -1
- package/src/cli/account.ts +1 -1
- package/src/cli/capabilities-agents-routing.ts +45 -8
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-provider-models.ts +5 -4
- package/src/cli/combo-result.ts +8 -2
- package/src/cli/combo.ts +17 -0
- package/src/cli/cross-home-owner.ts +16 -2
- package/src/cli/help.ts +1 -1
- package/src/cli/loopback-family.ts +31 -0
- package/src/cli/message-command.ts +4 -1
- package/src/cli/message-remote-args.ts +69 -0
- package/src/cli/message-remote-command.ts +77 -0
- package/src/cli/provider-runtime.ts +69 -1
- package/src/cli/registry.ts +3 -3
- package/src/codex/account-pause.ts +6 -1
- package/src/codex/account-runtime-state.ts +5 -0
- package/src/codex/account-store.ts +30 -3
- package/src/codex/auth-api/account-list.ts +3 -0
- package/src/codex/auth-api/main-account-probe.ts +3 -2
- package/src/codex/auth-context.ts +9 -4
- package/src/codex/catalog/aggregation.ts +15 -0
- package/src/codex/catalog/auto-review.ts +2 -1
- package/src/codex/catalog/build-entries.ts +17 -7
- package/src/codex/catalog/derive-entry.ts +3 -1
- package/src/codex/catalog/metadata.ts +2 -0
- package/src/codex/catalog/model-visibility.ts +0 -2
- package/src/codex/catalog/ownership.ts +71 -0
- package/src/codex/catalog/parsing.ts +12 -3
- package/src/codex/catalog/retained-sync.ts +2 -1
- package/src/codex/catalog/routed-gather.ts +3 -1
- package/src/codex/catalog/routed-removal.ts +4 -4
- package/src/codex/convergence.ts +2 -1
- package/src/codex/low-quota-desktop-notifier.ts +125 -0
- package/src/codex/low-quota-protection.ts +57 -31
- package/src/codex/main-account-cache.ts +77 -3
- package/src/codex/main-account.ts +6 -2
- package/src/codex/native-residue.ts +2 -3
- package/src/codex/quota.ts +26 -2
- package/src/codex/websocket-activity.ts +161 -0
- package/src/codex/websocket-registry.ts +25 -1
- package/src/combos/failover.ts +2 -1
- package/src/combos/jev-model-backend.ts +30 -7
- package/src/combos/jev-model-request.ts +20 -0
- package/src/combos/jev-quota-activation.ts +153 -0
- package/src/combos/jev-quota-config.ts +62 -0
- package/src/combos/jev-quota-route.ts +26 -0
- package/src/combos/jev-quota.ts +88 -0
- package/src/combos/jev.ts +25 -12
- package/src/combos/types.ts +27 -0
- package/src/compatibility/index.ts +3 -0
- package/src/compatibility/openai-responses.ts +8 -1
- package/src/compatibility/opengateway-responses.ts +61 -0
- package/src/config/account-auto-pause-revisions.ts +136 -0
- package/src/config/diagnostics.ts +13 -0
- package/src/config/live-reconcile.ts +9 -0
- package/src/config/opencode-zen-upgrade.ts +9 -8
- package/src/config/persist-unlocked.ts +10 -1
- package/src/config/removed-opencode-free.ts +135 -0
- package/src/config/schema/account-auto-pause.ts +14 -0
- package/src/config/schema/config-schema.ts +15 -0
- package/src/config/schema/leaf-validators.ts +7 -0
- package/src/config.ts +10 -3
- package/src/generated/compatibility-version.json +411 -175
- package/src/images/loop.ts +101 -27
- package/src/lib/account-auto-pause-response.ts +16 -0
- package/src/lib/bun-runtime.ts +18 -1
- package/src/lib/bun-transpiler-cache-scope.d.mts +8 -0
- package/src/lib/bun-transpiler-cache-scope.mjs +68 -0
- package/src/lib/decision-quota-sink.ts +76 -0
- package/src/lib/request-failure-model.ts +2 -0
- package/src/lib/state-store-registrations.ts +25 -0
- package/src/link/ssh-argv.ts +3 -0
- package/src/messaging/envelope.ts +6 -3
- package/src/messaging/remote-auth.ts +83 -0
- package/src/messaging/remote-bridge.ts +270 -0
- package/src/messaging/remote-contract.ts +108 -0
- package/src/messaging/remote-enrollment-recovery.ts +59 -0
- package/src/messaging/remote-enrollment.ts +244 -0
- package/src/messaging/remote-envelope.ts +40 -0
- package/src/messaging/remote-files.ts +117 -0
- package/src/messaging/remote-owner.ts +169 -0
- package/src/messaging/remote-ports.ts +56 -0
- package/src/messaging/remote-process.ts +131 -0
- package/src/messaging/remote-replies.ts +98 -0
- package/src/messaging/remote-rpc-admission.ts +32 -0
- package/src/messaging/remote-send.ts +72 -0
- package/src/messaging/remote-store.ts +111 -0
- package/src/messaging/remote-tunnels.ts +65 -0
- package/src/messaging/rpc.ts +13 -1
- package/src/oauth/account-auto-pause-policy.ts +66 -0
- package/src/oauth/account-auto-pause.ts +171 -0
- package/src/oauth/account-quota-rank.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +6 -0
- package/src/oauth/anthropic-model-family.ts +8 -0
- package/src/oauth/anthropic-model-quota.ts +2 -6
- package/src/oauth/api-key-pool-preserve.ts +37 -0
- package/src/oauth/generic-account-failover.ts +7 -4
- package/src/oauth/index.ts +5 -35
- package/src/oauth/store.ts +35 -8
- package/src/oauth/types.ts +2 -0
- package/src/plugins/upstream-hooks.ts +14 -1
- package/src/protocols/encoders/adapter-events.ts +5 -5
- package/src/protocols/encoders/messages.ts +2 -2
- package/src/providers/api-key-pause.ts +32 -0
- package/src/providers/api-key-resolve.ts +10 -4
- package/src/providers/api-key-selection.ts +6 -1
- package/src/providers/api-keys.ts +8 -4
- package/src/providers/claude-cli-usage.ts +5 -0
- package/src/providers/key-failover.ts +11 -11
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/opencode-zen-rate-limit.ts +12 -73
- package/src/providers/quota/account-cache.ts +15 -1
- package/src/providers/quota/anthropic-account-quota.ts +2 -1
- package/src/providers/quota/anthropic-cooldown-recovery.ts +22 -4
- package/src/providers/quota/anthropic-decision-windows.ts +47 -0
- package/src/providers/quota/antigravity.ts +39 -6
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +3 -3
- package/src/providers/quota-decision-publication.ts +77 -0
- package/src/providers/quota-decision-snapshot.ts +145 -0
- package/src/providers/quota-routing-cache.ts +3 -0
- package/src/providers/quota-types.ts +6 -1
- package/src/providers/quota.ts +3 -2
- package/src/providers/registry/entries-extended.ts +8 -52
- package/src/providers/registry/model-seeds.ts +1 -1
- package/src/providers/registry.ts +1 -2
- package/src/providers/stale-vision-classification-migration.ts +1 -1
- package/src/responses/parser.ts +18 -10
- package/src/responses/reasoning-replay-cache.ts +50 -4
- package/src/responses/web-search-replay.ts +138 -0
- package/src/routing/analytics.ts +7 -15
- package/src/routing/compatibility/behavior.ts +4 -1
- package/src/server/auth-cors.ts +7 -0
- package/src/server/background-lifecycle.ts +2 -1
- package/src/server/chat-native.ts +28 -13
- package/src/server/claude-messages.ts +4 -2
- package/src/server/index/live-sideband.ts +3 -2
- package/src/server/index/optional-listeners.ts +6 -0
- package/src/server/index/serve-options.ts +11 -5
- package/src/server/index/websocket-handler.ts +18 -4
- package/src/server/index.ts +7 -7
- package/src/server/lifecycle.ts +13 -1
- package/src/server/management/combo-routes.ts +17 -2
- package/src/server/management/oauth-account-routes.ts +10 -1
- package/src/server/management/provider-completion-test.ts +289 -0
- package/src/server/management/provider-routes.ts +9 -1
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/messages-native.ts +34 -7
- package/src/server/messages-shared.ts +4 -1
- package/src/server/request-log.ts +7 -2
- package/src/server/responses/account-auto-pause-recovery.ts +26 -0
- package/src/server/responses/adapter-continuation.ts +34 -18
- package/src/server/responses/adapter-delivery.ts +53 -6
- package/src/server/responses/adapter-dispatch.ts +104 -29
- package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
- package/src/server/responses/combo-session-recall.ts +25 -0
- package/src/server/responses/combo-stream-preflight.ts +6 -3
- package/src/server/responses/combo-tool-loop-pin.ts +70 -0
- package/src/server/responses/compact.ts +8 -5
- package/src/server/responses/compaction-recovery.ts +2 -0
- package/src/server/responses/context-overflow.ts +141 -2
- package/src/server/responses/core-codex-account.ts +9 -3
- package/src/server/responses/core-combo.ts +34 -7
- package/src/server/responses/core-lifetime.ts +3 -2
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/core-replay.ts +5 -1
- package/src/server/responses/core.ts +12 -9
- package/src/server/responses/empty-completion-guard.ts +2 -0
- package/src/server/responses/fetch-helpers.ts +35 -4
- package/src/server/responses/jev-model-invoke.ts +13 -14
- package/src/server/responses/native-steering-log.ts +2 -2
- package/src/server/responses/passthrough-delivery.ts +3 -2
- package/src/server/responses/passthrough-dispatch.ts +30 -33
- package/src/server/responses/passthrough-execution.ts +3 -2
- package/src/server/responses/passthrough-observations.ts +38 -0
- package/src/server/responses/request-compression.ts +98 -0
- package/src/server/responses/request-prepare.ts +1 -0
- package/src/server/responses/request-transport.ts +32 -3
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/response-tool-ownership.ts +21 -0
- package/src/server/responses/run-turn-execution.ts +2 -0
- package/src/server/responses/sidecar-execution.ts +59 -10
- package/src/server/responses/sidecar-send-budget.ts +2 -2
- package/src/server/responses/terminal-guard.ts +2 -0
- package/src/server/responses-undeclared-tool-guard.ts +1 -1
- package/src/server/startup-action-control.ts +2 -2
- package/src/server/startup-health-cache.ts +2 -2
- package/src/server/windows-tray-control.ts +2 -2
- package/src/server/ws-bridge.ts +2 -0
- package/src/service/definition.ts +9 -1
- package/src/types/account-auto-pause.ts +15 -0
- package/src/types/config.ts +23 -1
- package/src/types/opencode-zen.ts +9 -3
- package/src/types/provider.ts +9 -5
- package/src/types/request.ts +30 -4
- package/src/types/wire.ts +4 -2
- package/src/types.ts +2 -0
- package/src/usage/cost.ts +17 -1
- package/src/usage/expected-prices.ts +3 -1
- package/src/usage/jev-stats.ts +4 -0
- package/src/usage/log.ts +2 -0
- package/src/usage/summary.ts +16 -3
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/web-search/anthropic-history.ts +59 -0
- package/src/web-search/loop.ts +212 -73
- package/src/web-search/progress-stream.ts +14 -1
- package/src/web-search/run-turn-loop.ts +9 -3
- package/src/web-search/sidecar-search-replay.ts +96 -0
- package/gui/dist/assets/App-ZfYbLHkB.js +0 -58
- package/gui/dist/assets/App-wT3mXqFz.css +0 -1
- package/gui/dist/assets/index-CfYDMj34.js +0 -94
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { captureAnthropicRejectionPayloads, keepAnthropicReplayBlock } from "./anthropic-rejection-memo";
|
|
2
|
+
import { anthropicDeclaredTools } from "./anthropic-tool-catalog";
|
|
3
|
+
import { anthropicReplayContent, createAnthropicReplayCapture } from "./anthropic-replay";
|
|
1
4
|
import { withAnthropicProgressUpdates } from "./anthropic-progress";
|
|
2
5
|
import type { IncomingMeta, ProviderAdapter } from "./base";
|
|
3
6
|
import { createAdapterTierMetadata, type AdapterTierMetadata } from "../providers/fastwire";
|
|
@@ -17,7 +20,7 @@ import type {
|
|
|
17
20
|
OcxToolResultMessage,
|
|
18
21
|
OcxUsage,
|
|
19
22
|
} from "../types";
|
|
20
|
-
import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName
|
|
23
|
+
import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName } from "../types";
|
|
21
24
|
import { ANTHROPIC_OAUTH_BETA, CLAUDE_CODE_SYSTEM_INSTRUCTION, applyClaudeToolPrefix, stripClaudeToolPrefix } from "../oauth/anthropic";
|
|
22
25
|
import { parseDataUrl } from "./image";
|
|
23
26
|
import { enforceAnthropicImageLimits } from "./anthropic-image-guard";
|
|
@@ -32,6 +35,7 @@ import { decodeServerSentEvents } from "../lib/sse-decoder";
|
|
|
32
35
|
import { isTranslatorBudgetExceededError, retainTranslatedEventBatch, type TranslatorBudget } from "../lib/translator-budget";
|
|
33
36
|
import { isReasoningEffortOmitted, modelRecordValue } from "../reasoning-effort";
|
|
34
37
|
import { applyAgentRouterLanguageFraming, isAgentRouterEndpoint } from "./agentrouter";
|
|
38
|
+
import { anthropicCacheWrite1hTokens, resolveAnthropicCacheRetention, type AnthropicCacheRetention } from "./anthropic/cache-retention";
|
|
35
39
|
import { claudeFamilyVersion, rejectsCombinedSampling, rejectsForcedToolChoice, rejectsSamplingParameters, supportsExplicitThinkingDisable, usesAdaptiveThinking, usesBetweenToolsFloor } from "./anthropic-model-contract";
|
|
36
40
|
|
|
37
41
|
/** Map a user content part to an Anthropic content block (text or image source). */
|
|
@@ -73,8 +77,12 @@ const COMPAT_TOOL_PREFIX = "cx_";
|
|
|
73
77
|
type CacheControl = { type: "ephemeral"; ttl?: "1h" | "5m" };
|
|
74
78
|
const MAX_CACHE_BREAKPOINTS = 4;
|
|
75
79
|
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
/** Omitted retention follows the destination default (1h on first-party Anthropic, else 5m). */
|
|
81
|
+
function resolveCacheControl(
|
|
82
|
+
retention: AnthropicCacheRetention | undefined,
|
|
83
|
+
provider: OcxProviderConfig,
|
|
84
|
+
): CacheControl | undefined {
|
|
85
|
+
const r = resolveAnthropicCacheRetention(retention, provider);
|
|
78
86
|
if (r === "none") return undefined;
|
|
79
87
|
return r === "long" ? { type: "ephemeral", ttl: "1h" } : { type: "ephemeral" };
|
|
80
88
|
}
|
|
@@ -283,7 +291,7 @@ function normalizeTtlOrdering(body: Record<string, unknown>): void {
|
|
|
283
291
|
}
|
|
284
292
|
}
|
|
285
293
|
|
|
286
|
-
function isLikelyRealAnthropicThinkingSignature(signature: string | undefined): signature is string {
|
|
294
|
+
export function isLikelyRealAnthropicThinkingSignature(signature: string | undefined): signature is string {
|
|
287
295
|
if (typeof signature !== "string" || signature.length < 16) return false;
|
|
288
296
|
if (/^(fc|call|msg|rs|resp|reasoning|item|ws|tool|func|function)[-_]/i.test(signature)) return false;
|
|
289
297
|
return /^[A-Za-z0-9+/_=-]+$/.test(signature);
|
|
@@ -522,13 +530,13 @@ function streamedToolArgumentsParse(assembled: string): boolean {
|
|
|
522
530
|
}
|
|
523
531
|
}
|
|
524
532
|
|
|
525
|
-
/** A
|
|
526
|
-
function
|
|
533
|
+
/** A server tool's complete streamed input; undefined when it cannot be recovered. */
|
|
534
|
+
function serverToolInput(json: string): Record<string, unknown> | undefined {
|
|
527
535
|
try {
|
|
528
|
-
const
|
|
529
|
-
return
|
|
536
|
+
const input = JSON.parse(json) as unknown;
|
|
537
|
+
return isAnthropicRecord(input) ? input : undefined;
|
|
530
538
|
} catch {
|
|
531
|
-
return
|
|
539
|
+
return undefined;
|
|
532
540
|
}
|
|
533
541
|
}
|
|
534
542
|
|
|
@@ -640,6 +648,7 @@ function usageFromAnthropic(usage: unknown): OcxUsage | undefined {
|
|
|
640
648
|
// estimate from visible summaries. Invalid optional detail leaves totals intact.
|
|
641
649
|
const thinking = isAnthropicRecord(usage.output_tokens_details) ? usage.output_tokens_details.thinking_tokens : undefined;
|
|
642
650
|
const reasoningOutputTokens = typeof thinking === "number" && Number.isFinite(thinking) && thinking >= 0 && thinking <= output ? thinking : undefined;
|
|
651
|
+
const write1h = anthropicCacheWrite1hTokens(usage, write);
|
|
643
652
|
return {
|
|
644
653
|
inputTokens,
|
|
645
654
|
outputTokens: output,
|
|
@@ -648,6 +657,7 @@ function usageFromAnthropic(usage: unknown): OcxUsage | undefined {
|
|
|
648
657
|
cachedInputTokens: read,
|
|
649
658
|
cacheReadInputTokens: read,
|
|
650
659
|
cacheCreationInputTokens: write,
|
|
660
|
+
...(write1h !== undefined ? { cacheCreation1hInputTokens: write1h } : {}),
|
|
651
661
|
} : {}),
|
|
652
662
|
};
|
|
653
663
|
}
|
|
@@ -735,7 +745,16 @@ function orphanToolResultContent(msg: OcxToolResultMessage): string | unknown[]
|
|
|
735
745
|
function messagesToAnthropicFormat(
|
|
736
746
|
parsed: OcxParsedRequest,
|
|
737
747
|
toolNames: { toWire: (name: string) => string },
|
|
748
|
+
serverReplay?: ReadonlyMap<string, readonly Record<string, unknown>[]>,
|
|
738
749
|
): { system: string | undefined; messages: unknown[] } {
|
|
750
|
+
// The client's server-tool exchanges go back ahead of the block they preceded. Their anchors are
|
|
751
|
+
// unique in the caller's history (replayedAnthropicServerBlocks drops repeated ones); each is used once.
|
|
752
|
+
const pendingReplay = new Map(serverReplay);
|
|
753
|
+
const replayed = (anchor: string) => {
|
|
754
|
+
const blocks = pendingReplay.get(anchor) ?? [];
|
|
755
|
+
pendingReplay.delete(anchor);
|
|
756
|
+
return blocks;
|
|
757
|
+
};
|
|
739
758
|
// One allocator for the whole request: a tool_result must resolve to the SAME wire id its
|
|
740
759
|
// call got, and two distinct raw ids must never collapse into one. Conforming ids are claimed
|
|
741
760
|
// first so a rewritten id can never squat on an id another call legitimately owns.
|
|
@@ -750,8 +769,8 @@ function messagesToAnthropicFormat(
|
|
|
750
769
|
}
|
|
751
770
|
}
|
|
752
771
|
const toolCatalogNudge = buildNonOpenAIToolCatalogNudgeForTools(
|
|
753
|
-
parsed
|
|
754
|
-
|
|
772
|
+
anthropicDeclaredTools(parsed),
|
|
773
|
+
undefined,
|
|
755
774
|
tool => toolNames.toWire(namespacedToolName(tool.namespace, tool.name)),
|
|
756
775
|
);
|
|
757
776
|
const systemParts = [...(parsed.context.systemPrompt ?? []), ...(toolCatalogNudge ? [toolCatalogNudge] : [])];
|
|
@@ -781,6 +800,7 @@ function messagesToAnthropicFormat(
|
|
|
781
800
|
case "assistant": {
|
|
782
801
|
const aMsg = msg as OcxAssistantMessage;
|
|
783
802
|
const preface: unknown[] = [];
|
|
803
|
+
|
|
784
804
|
const toolUses: unknown[] = [];
|
|
785
805
|
const originalOrder: unknown[] = [];
|
|
786
806
|
const appendPreface = (block: unknown) => { preface.push(block); originalOrder.push(block); };
|
|
@@ -789,9 +809,13 @@ function messagesToAnthropicFormat(
|
|
|
789
809
|
for (const part of aMsg.content) {
|
|
790
810
|
if (part.type === "text") {
|
|
791
811
|
const text = (part as OcxTextContent).text;
|
|
812
|
+
for (const b of replayed(`text:${text}`)) appendPreface(b);
|
|
792
813
|
if (text) appendPreface({ type: "text", text });
|
|
793
814
|
} else if (part.type === "thinking") {
|
|
794
815
|
const t = part as OcxThinkingContent;
|
|
816
|
+
// Server blocks that preceded this reasoning keep their place, even when it is dropped below.
|
|
817
|
+
for (const data of t.redacted ?? []) for (const b of replayed(`redacted:${data}`)) appendPreface(b);
|
|
818
|
+
for (const b of replayed(`thinking:${t.signature}`)) appendPreface(b);
|
|
795
819
|
// History minted under another serving identity (or already rejected as opaque) is not
|
|
796
820
|
// this destination's to verify: drop its opaque blocks, as the Responses passthrough does.
|
|
797
821
|
if (parsed._stripReasoningEncryptedContent === true) continue;
|
|
@@ -806,6 +830,7 @@ function messagesToAnthropicFormat(
|
|
|
806
830
|
}
|
|
807
831
|
} else if (part.type === "toolCall") {
|
|
808
832
|
const tc = part as OcxToolCall;
|
|
833
|
+
for (const b of replayed(`call:${tc.id}`)) appendPreface(b);
|
|
809
834
|
const flatName = namespacedToolName(tc.namespace, tc.name);
|
|
810
835
|
// Normalized here, and identically for the matching tool_result above, so a history
|
|
811
836
|
// replayed from another provider path keeps its call/result pairing (#1767).
|
|
@@ -824,9 +849,15 @@ function messagesToAnthropicFormat(
|
|
|
824
849
|
}
|
|
825
850
|
// Anthropic treats text/thinking after tool_use as ending the tool turn, which makes
|
|
826
851
|
// earlier tool_use ids look unpaired (#620 / common multi-step history shape).
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
852
|
+
const captured = anthropicReplayContent(aMsg);
|
|
853
|
+
if (captured) {
|
|
854
|
+
const expected = toolUses as Array<{ id: string; name: string }>;
|
|
855
|
+
const actual = captured.filter(block => block.type === "tool_use");
|
|
856
|
+
if (actual.length !== expected.length || actual.some((block, index) => block.id !== expected[index]?.id || block.name !== expected[index]?.name)) {
|
|
857
|
+
throw new Error("Anthropic search replay tool identity changed");
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
const content = (captured ?? (signed ? originalOrder : [...preface, ...toolUses])).filter(block => keepAnthropicReplayBlock(parsed, block as Record<string, unknown>));
|
|
830
861
|
if (content.length === 0) break;
|
|
831
862
|
messages.push({ role: "assistant", content });
|
|
832
863
|
if (toolUseIds.length > 0) {
|
|
@@ -889,9 +920,7 @@ function messagesToAnthropicFormat(
|
|
|
889
920
|
|
|
890
921
|
function toolsToAnthropicFormat(parsed: OcxParsedRequest, toolNames: { toWire: (name: string) => string }): unknown[] | undefined {
|
|
891
922
|
if (!parsed.context.tools || parsed.context.tools.length === 0) return undefined;
|
|
892
|
-
const tools =
|
|
893
|
-
? parsed.context.tools.filter(toolChoiceToolPredicate(parsed.options.toolChoice, parsed.context.tools))
|
|
894
|
-
: parsed.context.tools;
|
|
923
|
+
const tools = anthropicDeclaredTools(parsed);
|
|
895
924
|
if (tools.length === 0) return undefined;
|
|
896
925
|
const converted = tools.map(t => ({
|
|
897
926
|
name: toolNames.toWire(namespacedToolName(t.namespace, t.name)),
|
|
@@ -998,7 +1027,9 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
998
1027
|
throw new Error("anthropic provider requires a non-empty apiKey (authMode: key)");
|
|
999
1028
|
}
|
|
1000
1029
|
|
|
1001
|
-
|
|
1030
|
+
// Only api.anthropic.com runs Anthropic's server tools, so only it gets their history back.
|
|
1031
|
+
const { system, messages } = messagesToAnthropicFormat(parsed, toolNames,
|
|
1032
|
+
usesNativeAnthropicEndpoint(provider) ? parsed._anthropicServerReplay : undefined);
|
|
1002
1033
|
// Before image normalization, so the framing block is present for every downstream pass.
|
|
1003
1034
|
if (isAgentRouterEndpoint(provider.baseUrl)) applyAgentRouterLanguageFraming(messages);
|
|
1004
1035
|
// Primary image layer: resize/re-encode to fit Anthropic limits without dropping
|
|
@@ -1199,7 +1230,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
1199
1230
|
|
|
1200
1231
|
// Prompt caching: native Anthropic supports top-level automatic caching, which
|
|
1201
1232
|
// follows the moving final block across turns. Keep one breakpoint slot free for it.
|
|
1202
|
-
const cc = resolveCacheControl(cacheRetention);
|
|
1233
|
+
const cc = resolveCacheControl(cacheRetention, provider);
|
|
1203
1234
|
const automaticPromptCaching = cc && usesNativeAnthropicEndpoint(provider);
|
|
1204
1235
|
if (automaticPromptCaching) body.cache_control = cc;
|
|
1205
1236
|
const explicitLimit = automaticPromptCaching ? MAX_CACHE_BREAKPOINTS - 1 : MAX_CACHE_BREAKPOINTS;
|
|
@@ -1210,7 +1241,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
1210
1241
|
enforceCacheControlLimit(body, explicitLimit);
|
|
1211
1242
|
normalizeTtlOrdering(body);
|
|
1212
1243
|
|
|
1213
|
-
|
|
1244
|
+
const request = {
|
|
1214
1245
|
url,
|
|
1215
1246
|
method: "POST",
|
|
1216
1247
|
headers,
|
|
@@ -1224,6 +1255,8 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
1224
1255
|
fastSpeed?.value ?? null,
|
|
1225
1256
|
),
|
|
1226
1257
|
};
|
|
1258
|
+
captureAnthropicRejectionPayloads(request, body, incoming?.translatorBudget);
|
|
1259
|
+
return request;
|
|
1227
1260
|
},
|
|
1228
1261
|
|
|
1229
1262
|
async *parseStream(
|
|
@@ -1236,6 +1269,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
1236
1269
|
return;
|
|
1237
1270
|
}
|
|
1238
1271
|
|
|
1272
|
+
const replayCapture = createAnthropicReplayCapture(response, budget);
|
|
1239
1273
|
const budgetEncoder = new TextEncoder();
|
|
1240
1274
|
let currentBlockType = "";
|
|
1241
1275
|
let currentToolCallId = "";
|
|
@@ -1270,11 +1304,13 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
1270
1304
|
yield anthropicContentFilterIncomplete(pendingStopReason, usageFromAnthropic(pendingUsage));
|
|
1271
1305
|
return;
|
|
1272
1306
|
}
|
|
1273
|
-
|
|
1307
|
+
const done: AdapterEvent = {
|
|
1274
1308
|
type: "done",
|
|
1275
1309
|
usage: usageFromAnthropic(pendingUsage),
|
|
1276
1310
|
...(pendingStopReason ? { stopReason: pendingStopReason } : {}),
|
|
1277
1311
|
};
|
|
1312
|
+
replayCapture?.finish(done);
|
|
1313
|
+
yield done;
|
|
1278
1314
|
};
|
|
1279
1315
|
|
|
1280
1316
|
try {
|
|
@@ -1294,6 +1330,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
1294
1330
|
try {
|
|
1295
1331
|
parsed = JSON.parse(payload);
|
|
1296
1332
|
} catch {
|
|
1333
|
+
replayCapture?.invalidate();
|
|
1297
1334
|
debugDroppedFrame("anthropic", payload);
|
|
1298
1335
|
continue;
|
|
1299
1336
|
}
|
|
@@ -1301,10 +1338,12 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
1301
1338
|
// it and the `data.type` read below crashed the stream. Drop a non-record frame the same
|
|
1302
1339
|
// way an unparseable one is dropped, so the message_stop check still governs the outcome.
|
|
1303
1340
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1341
|
+
replayCapture?.invalidate();
|
|
1304
1342
|
debugDroppedFrame("anthropic", payload);
|
|
1305
1343
|
continue;
|
|
1306
1344
|
}
|
|
1307
1345
|
const data = parsed as Record<string, unknown>;
|
|
1346
|
+
replayCapture?.accept({ ...data, type: record.event || data.type });
|
|
1308
1347
|
|
|
1309
1348
|
switch (record.event || data.type) {
|
|
1310
1349
|
case "message_start": {
|
|
@@ -1317,6 +1356,9 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
1317
1356
|
case "content_block_start": {
|
|
1318
1357
|
const block = data.content_block as { type: string; id?: string; name?: unknown; data?: string; thinking?: string; tool_use_id?: unknown; content?: unknown } | undefined;
|
|
1319
1358
|
if (!block) break;
|
|
1359
|
+
// Even an unmodeled server tool or empty block is semantic progress.
|
|
1360
|
+
// Fence replays before a later refusal; unknown blocks must not look like pings.
|
|
1361
|
+
yield { type: "heartbeat", replayUnsafe: true };
|
|
1320
1362
|
currentBlockType = block.type;
|
|
1321
1363
|
if (block.type === "thinking") {
|
|
1322
1364
|
// Preserve even a display:omitted block boundary. The bridge can then
|
|
@@ -1336,36 +1378,26 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
1336
1378
|
// no result in this response and must not surface as a completed search.
|
|
1337
1379
|
serverSearch = { id: block.id, json: "", streaming: true };
|
|
1338
1380
|
}
|
|
1339
|
-
if (block.type === "web_search_tool_result" && typeof block.tool_use_id === "string") {
|
|
1381
|
+
if ((block.type === "web_search_tool_result" || block.type === "web_fetch_tool_result") && typeof block.tool_use_id === "string") {
|
|
1382
|
+
// The begin/end pair the sidecar emits, plus both blocks verbatim so the Claude client holds a
|
|
1383
|
+
// replayable copy. Without this response's matching call and complete input, drop the
|
|
1384
|
+
// exchange instead of inventing a call that could poison the client's next request.
|
|
1385
|
+
const fetch = block.type === "web_fetch_tool_result";
|
|
1386
|
+
const input = serverSearch?.id === block.tool_use_id ? serverToolInput(serverSearch.json) : undefined;
|
|
1387
|
+
if (!input) break;
|
|
1340
1388
|
const hits = Array.isArray(block.content) ? block.content as Array<{ url?: unknown; title?: unknown }> : undefined;
|
|
1341
|
-
// The begin/end pair the sidecar emits. A result whose call came in an earlier response has no query here.
|
|
1342
|
-
const query = serverSearch?.id === block.tool_use_id ? serverToolInputField(serverSearch.json, "query") : "";
|
|
1343
1389
|
yield { type: "web_search_call_begin", id: block.tool_use_id };
|
|
1344
1390
|
yield {
|
|
1345
1391
|
type: "web_search_call_end",
|
|
1346
1392
|
id: block.tool_use_id,
|
|
1347
|
-
queries: query ? [query] : [],
|
|
1348
|
-
// A failed
|
|
1349
|
-
status: hits ? "completed" : "failed",
|
|
1393
|
+
queries: typeof input.query === "string" && input.query.length > 0 ? [input.query] : [],
|
|
1394
|
+
// A failed call carries one error object instead of the hit list / fetched page.
|
|
1395
|
+
status: (fetch ? (block.content as { type?: unknown } | undefined)?.type === "web_fetch_result" : hits) ? "completed" : "failed",
|
|
1350
1396
|
sources: (hits ?? []).flatMap(h => typeof h.url === "string"
|
|
1351
1397
|
? [{ url: h.url, ...(typeof h.title === "string" ? { title: h.title } : {}) }] : []),
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
if (block.type === "web_fetch_tool_result" && typeof block.tool_use_id === "string") {
|
|
1356
|
-
// Relayed as the same cell, opened at the result for the same reason; the result is
|
|
1357
|
-
// carried verbatim (a web_fetch_result document or a web_fetch_tool_error).
|
|
1358
|
-
const result = block.content ?? { type: "web_fetch_tool_error", error_code: "unavailable" };
|
|
1359
|
-
const url = (result as { url?: unknown }).url;
|
|
1360
|
-
yield { type: "web_search_call_begin", id: block.tool_use_id };
|
|
1361
|
-
yield {
|
|
1362
|
-
type: "web_search_call_end",
|
|
1363
|
-
id: block.tool_use_id,
|
|
1364
|
-
queries: [],
|
|
1365
|
-
status: (result as { type?: unknown }).type === "web_fetch_result" ? "completed" : "failed",
|
|
1366
|
-
fetch: {
|
|
1367
|
-
url: typeof url === "string" ? url : serverSearch?.id === block.tool_use_id ? serverToolInputField(serverSearch.json, "url") : "",
|
|
1368
|
-
result,
|
|
1398
|
+
anthropic: {
|
|
1399
|
+
use: { type: "server_tool_use", id: block.tool_use_id, name: fetch ? "web_fetch" : "web_search", input },
|
|
1400
|
+
result: block as Record<string, unknown>,
|
|
1369
1401
|
},
|
|
1370
1402
|
};
|
|
1371
1403
|
serverSearch = undefined;
|
|
@@ -1415,8 +1447,8 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
1415
1447
|
yield { type: "tool_call_delta", arguments: delta.partial_json };
|
|
1416
1448
|
} else if (delta.type === "input_json_delta" && typeof delta.partial_json === "string" && serverSearch?.streaming
|
|
1417
1449
|
&& serverSearch.json.length < 4096) {
|
|
1418
|
-
// Capped:
|
|
1419
|
-
serverSearch.json += delta.partial_json;
|
|
1450
|
+
// Capped: input that cannot be recovered drops its exchange instead of inventing a call.
|
|
1451
|
+
serverSearch.json += delta.partial_json.slice(0, 4096 - serverSearch.json.length);
|
|
1420
1452
|
}
|
|
1421
1453
|
break;
|
|
1422
1454
|
}
|
|
@@ -1463,8 +1495,13 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
1463
1495
|
break;
|
|
1464
1496
|
}
|
|
1465
1497
|
case "error": {
|
|
1466
|
-
const err = data.error as { message?:
|
|
1467
|
-
yield { type: "error", message: err?.message
|
|
1498
|
+
const err = data.error as { message?: unknown; type?: unknown; code?: unknown; status?: unknown } | undefined;
|
|
1499
|
+
yield { type: "error", message: typeof err?.message === "string" ? err.message : "Anthropic error",
|
|
1500
|
+
...(typeof err?.type === "string" ? { errorType: err.type } : {}),
|
|
1501
|
+
...(typeof err?.code === "string" ? { code: err.code } : {}),
|
|
1502
|
+
...(typeof err?.status === "number" && Number.isInteger(err.status) && err.status >= 400 && err.status <= 599
|
|
1503
|
+
? { status: err.status } : {}),
|
|
1504
|
+
};
|
|
1468
1505
|
return;
|
|
1469
1506
|
}
|
|
1470
1507
|
}
|
|
@@ -1483,6 +1520,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
1483
1520
|
// one-terminal-event contract for consumers that keep draining the generator.
|
|
1484
1521
|
return;
|
|
1485
1522
|
} finally {
|
|
1523
|
+
replayCapture?.discard();
|
|
1486
1524
|
if (currentToolCallId) budget.closeCall(currentToolCallId);
|
|
1487
1525
|
}
|
|
1488
1526
|
if (!emittedDone) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { namespacedToolName, type AdapterEvent, type OcxMessage, type OcxParsedRequest, type OcxUsage } from "../../types";
|
|
2
2
|
import type { TranslatorBudget } from "../../lib/translator-budget";
|
|
3
3
|
import { isValidCodingAgentToolNamePart } from "./tool-catalog";
|
|
4
|
+
import { anthropicCacheWrite1hTokens } from "../anthropic/cache-retention";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Shared stream-json protocol for official coding-agent CLIs (CodeBuddy Code and Qoder CLI).
|
|
@@ -165,6 +166,7 @@ function usageFromAnthropicShape(usage: Record<string, unknown>): OcxUsage | und
|
|
|
165
166
|
// that could carry those tokens instead, so the turn under-reported usage and cost.
|
|
166
167
|
const cacheReadTotal = count(usage.cache_read_input_tokens);
|
|
167
168
|
const cacheCreationTotal = count(usage.cache_creation_input_tokens);
|
|
169
|
+
const cacheCreation1h = anthropicCacheWrite1hTokens(usage, cacheCreationTotal);
|
|
168
170
|
const inputTokens = freshInputTokens + cacheReadTotal + cacheCreationTotal;
|
|
169
171
|
if (inputTokens === 0 && outputTokens === 0 && cacheReadTotal === 0 && cacheCreationTotal === 0) {
|
|
170
172
|
return undefined;
|
|
@@ -175,6 +177,7 @@ function usageFromAnthropicShape(usage: Record<string, unknown>): OcxUsage | und
|
|
|
175
177
|
totalTokens: inputTokens + outputTokens,
|
|
176
178
|
...(cacheReadTotal > 0 ? { cachedInputTokens: cacheReadTotal, cacheReadInputTokens: cacheReadTotal } : {}),
|
|
177
179
|
...(cacheCreationTotal > 0 ? { cacheCreationInputTokens: cacheCreationTotal } : {}),
|
|
180
|
+
...(cacheCreationTotal > 0 && cacheCreation1h !== undefined ? { cacheCreation1hInputTokens: cacheCreation1h } : {}),
|
|
178
181
|
};
|
|
179
182
|
}
|
|
180
183
|
|
|
@@ -204,6 +207,8 @@ function mergePartialUsage(previous: OcxUsage | undefined, next: OcxUsage): OcxU
|
|
|
204
207
|
next.cacheReadInputTokens ?? next.cachedInputTokens ?? 0,
|
|
205
208
|
);
|
|
206
209
|
const cacheCreation = Math.max(previous.cacheCreationInputTokens ?? 0, next.cacheCreationInputTokens ?? 0);
|
|
210
|
+
const cacheCreation1h = Math.min(cacheCreation,
|
|
211
|
+
Math.max(previous.cacheCreation1hInputTokens ?? 0, next.cacheCreation1hInputTokens ?? 0));
|
|
207
212
|
// Inclusive inputs: fold each snapshot's fresh part, then add the folded cache counters once.
|
|
208
213
|
const freshInput = (usage: OcxUsage) => Math.max(0, usage.inputTokens
|
|
209
214
|
- (usage.cacheReadInputTokens ?? usage.cachedInputTokens ?? 0) - (usage.cacheCreationInputTokens ?? 0));
|
|
@@ -214,6 +219,7 @@ function mergePartialUsage(previous: OcxUsage | undefined, next: OcxUsage): OcxU
|
|
|
214
219
|
totalTokens: inputTokens + outputTokens,
|
|
215
220
|
...(cacheRead > 0 ? { cachedInputTokens: cacheRead, cacheReadInputTokens: cacheRead } : {}),
|
|
216
221
|
...(cacheCreation > 0 ? { cacheCreationInputTokens: cacheCreation } : {}),
|
|
222
|
+
...(cacheCreation1h > 0 ? { cacheCreation1hInputTokens: cacheCreation1h } : {}),
|
|
217
223
|
};
|
|
218
224
|
}
|
|
219
225
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { inheritAccountAutoPauseResponse } from "../lib/account-auto-pause-response";
|
|
1
2
|
import type { AdapterFetchContext, AdapterRequest } from "./base";
|
|
2
3
|
import { createAdapterPhysicalSend } from "./physical-send";
|
|
3
4
|
import type { SendClass } from "../lib/request-execution-budget";
|
|
@@ -38,10 +39,10 @@ async function normalizeFinalGoogleError(label: string, res: Response, signal?:
|
|
|
38
39
|
const headers = new Headers(res.headers);
|
|
39
40
|
headers.delete("content-encoding");
|
|
40
41
|
headers.delete("content-length");
|
|
41
|
-
return new Response(safeGoogleHttpErrorMessage(label, res.status,
|
|
42
|
+
return inheritAccountAutoPauseResponse(res, new Response(safeGoogleHttpErrorMessage(label, res.status,
|
|
42
43
|
body.displaySafe && !body.truncated ? body.text : ""), {
|
|
43
44
|
status: res.status, statusText: res.statusText, headers,
|
|
44
|
-
});
|
|
45
|
+
}));
|
|
45
46
|
}
|
|
46
47
|
return normalizeUpstreamHttpErrorResponse(res, {
|
|
47
48
|
signal,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini progress-text history trimming.
|
|
3
|
+
*
|
|
4
|
+
* Gemini models, observed on Gemini 4 Argon over Cloud Code Assist, sometimes skip their thinking
|
|
5
|
+
* channel on a tool-calling turn and plan in the visible text instead. The provider sends that
|
|
6
|
+
* planning as an ordinary text part without a thought marker, for example a progress line in the
|
|
7
|
+
* user's language followed by "Let's read google.ts 580-1700 first." Once one turn like that is
|
|
8
|
+
* replayed as history, the model copies the pattern on the turns that follow, so the leak grows
|
|
9
|
+
* through the rest of the session. Replaying the same 300K-token request with the leaked tails
|
|
10
|
+
* removed from the history stopped the leak, while stronger system-prompt wording did not.
|
|
11
|
+
*
|
|
12
|
+
* The fix is on the history side only. Text that precedes the tool call of a replayed model turn is
|
|
13
|
+
* intermediate progress, and the Google brevity instruction already asks for a single short line
|
|
14
|
+
* there, so it is replayed as its lead sentence. A turn Codex marks `final_answer` and text after a
|
|
15
|
+
* tool call are replayed whole. Visible output is never rewritten, final
|
|
16
|
+
* answers (turns without tool calls) are replayed in full, and the split is punctuation-based so
|
|
17
|
+
* it works the same in every language without matching any wording.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Sentence end: ASCII terminal punctuation followed by whitespace or the end (so "google.ts" and
|
|
22
|
+
* "3.5" are not boundaries), a script's own sentence terminator (CJK 。!?, Devanagari
|
|
23
|
+
* ।॥, Arabic ۔؟, Armenian ։, Ethiopic ።, Myanmar ။), or a line break. Closing quotes, brackets and
|
|
24
|
+
* Markdown emphasis right after the punctuation belong to the sentence.
|
|
25
|
+
*/
|
|
26
|
+
const SENTENCE_END = /[.!?]["'\u201D\u2019)\]*_`]*(?=\s|$)|[\u3002\uFF01\uFF1F\u0964\u0965\u06D4\u061F\u0589\u1362\u104B]["'\u201D\u2019\u300D\u300F)\]*_`]*|\n/g;
|
|
27
|
+
|
|
28
|
+
/** A boundary that only closes a leading list marker ("1.", "a.") or a common abbreviation ("e.g."). */
|
|
29
|
+
const NOT_A_SENTENCE = /^(?:\d{1,3}|[a-z])\.$|(?:^|\s)(?:e\.g|i\.e|etc|vs)\.$/i;
|
|
30
|
+
|
|
31
|
+
/** Whether replayed progress text for this model is trimmed to its lead sentence. */
|
|
32
|
+
export function trimsGeminiProgressHistory(modelId: string): boolean {
|
|
33
|
+
return /gemini/i.test(modelId);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The lead sentence of a replayed progress text, or undefined when nothing usable remains.
|
|
38
|
+
* Text without a sentence boundary is already a single sentence and is returned whole.
|
|
39
|
+
*/
|
|
40
|
+
export function geminiProgressLead(text: string): string | undefined {
|
|
41
|
+
const trimmed = text.trim();
|
|
42
|
+
if (trimmed.length === 0) return undefined;
|
|
43
|
+
SENTENCE_END.lastIndex = 0;
|
|
44
|
+
let end = trimmed.length;
|
|
45
|
+
for (let match = SENTENCE_END.exec(trimmed); match; match = SENTENCE_END.exec(trimmed)) {
|
|
46
|
+
const boundary = match.index + match[0].length;
|
|
47
|
+
if (!NOT_A_SENTENCE.test(trimmed.slice(0, boundary).trim())) { end = boundary; break; }
|
|
48
|
+
}
|
|
49
|
+
const lead = trimmed.slice(0, end).trim();
|
|
50
|
+
// A code-fence opener is not a sentence; replaying it alone would leave an unclosed fence.
|
|
51
|
+
if (lead.length === 0 || lead.startsWith("```")) return undefined;
|
|
52
|
+
return lead;
|
|
53
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { googleVertexLocationConfigError } from "../providers/google-vertex-location";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Vertex endpoint selection, shared by inference and replay identity.
|
|
5
|
+
*
|
|
6
|
+
* API keys follow explicit provider configuration only: `provider.project` selects the project
|
|
7
|
+
* endpoint (location defaults to `global`), otherwise the projectless express endpoint is used.
|
|
8
|
+
* Ambient GOOGLE_CLOUD_PROJECT / GCLOUD_PROJECT / GOOGLE_CLOUD_LOCATION belong to ADC, so an
|
|
9
|
+
* unrelated gcloud default in the shell cannot move an express-mode key onto another project.
|
|
10
|
+
*/
|
|
11
|
+
export type VertexEndpointSelection =
|
|
12
|
+
| { kind: "express" }
|
|
13
|
+
| { kind: "project"; project: string; location: string };
|
|
14
|
+
|
|
15
|
+
type VertexEndpointConfig = { project?: string; location?: string };
|
|
16
|
+
|
|
17
|
+
export function selectVertexEndpoint(
|
|
18
|
+
provider: VertexEndpointConfig,
|
|
19
|
+
hasApiKey: boolean,
|
|
20
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
21
|
+
): VertexEndpointSelection {
|
|
22
|
+
// A blank or whitespace-only project is unset: it must not select a /projects/%20/ endpoint.
|
|
23
|
+
const configuredProject = provider.project?.trim() || undefined;
|
|
24
|
+
if (hasApiKey) {
|
|
25
|
+
if (!configuredProject) return { kind: "express" };
|
|
26
|
+
return { kind: "project", project: configuredProject, location: provider.location || "global" };
|
|
27
|
+
}
|
|
28
|
+
const project = configuredProject || env.GOOGLE_CLOUD_PROJECT || env.GCLOUD_PROJECT;
|
|
29
|
+
if (!project) throw new Error("Vertex AI requires a project id (provider.project or GOOGLE_CLOUD_PROJECT/GCLOUD_PROJECT).");
|
|
30
|
+
const location = provider.location || env.GOOGLE_CLOUD_LOCATION;
|
|
31
|
+
if (!location) throw new Error("Vertex AI requires a location (provider.location or GOOGLE_CLOUD_LOCATION).");
|
|
32
|
+
return { kind: "project", project, location };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Replay namespace for thought-signature continuation; mirrors the endpoint actually used.
|
|
37
|
+
* The endpoint kind is part of the key and the project is encoded, so no configured project
|
|
38
|
+
* (for example one literally named "api-key" or containing ":") can share express mode's space.
|
|
39
|
+
*/
|
|
40
|
+
export function vertexReplayModelKey(selection: VertexEndpointSelection, modelId: string): string {
|
|
41
|
+
return selection.kind === "express"
|
|
42
|
+
? `vertex:express:${modelId}`
|
|
43
|
+
: `vertex:project:${encodeURIComponent(selection.project)}:${selection.location}:${modelId}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const VERTEX_MODEL_PATH_REJECT = /[/?#\\%]/;
|
|
47
|
+
|
|
48
|
+
/** The model id is interpolated as one URL path segment after a fixed Google authority. */
|
|
49
|
+
export function vertexModelPathError(modelId: string): string | undefined {
|
|
50
|
+
if (!modelId || VERTEX_MODEL_PATH_REJECT.test(modelId) || modelId.includes("..")) {
|
|
51
|
+
return "Vertex AI model id must be a single path segment without '/', '?', '#', '\\', '%' or '..'.";
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Builds the inference URL for a selection; validates location before any authority is formed. */
|
|
57
|
+
export function vertexInferenceUrl(selection: VertexEndpointSelection, modelId: string, method: string, streamParam: string): string {
|
|
58
|
+
const modelError = vertexModelPathError(modelId);
|
|
59
|
+
if (modelError) throw new Error(modelError);
|
|
60
|
+
if (selection.kind === "express") {
|
|
61
|
+
return `https://aiplatform.googleapis.com/v1/publishers/google/models/${modelId}:${method}${streamParam}`;
|
|
62
|
+
}
|
|
63
|
+
const locationError = googleVertexLocationConfigError(selection.location);
|
|
64
|
+
if (locationError) throw new Error(locationError);
|
|
65
|
+
const host = selection.location === "global" ? "aiplatform.googleapis.com" : `${selection.location}-aiplatform.googleapis.com`;
|
|
66
|
+
return `https://${host}/v1/projects/${encodeURIComponent(selection.project)}/locations/${selection.location}/publishers/google/models/${modelId}:${method}${streamParam}`;
|
|
67
|
+
}
|