@bitkyc08/opencodex 2.86.0 → 2.87.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/App-ZfYbLHkB.js +58 -0
- package/gui/dist/assets/App-wT3mXqFz.css +1 -0
- package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-DCd-A1az.js} +1 -1
- package/gui/dist/assets/index-CfYDMj34.js +94 -0
- package/gui/dist/assets/index-DPeB9HpV.css +1 -0
- package/gui/dist/assets/quota-summary-ChvmKGOk.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +9 -8
- package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
- package/src/adapters/anthropic/passthrough.ts +7 -23
- package/src/adapters/anthropic-progress.ts +84 -0
- package/src/adapters/anthropic.ts +108 -21
- package/src/adapters/base.ts +2 -0
- package/src/adapters/claude-cli/adapter.ts +111 -23
- package/src/adapters/claude-cli/profiles.ts +2 -1
- package/src/adapters/claude-cli/stable-replay.ts +108 -0
- package/src/adapters/claude-cli/usage-admission.ts +230 -0
- package/src/adapters/coding-agent/protocol.ts +1 -1
- package/src/adapters/coding-agent/turn.ts +18 -2
- package/src/adapters/dsh-account.ts +97 -0
- package/src/adapters/google.ts +35 -8
- package/src/adapters/openai-chat/messages.ts +4 -3
- package/src/adapters/openai-chat/passthrough.ts +8 -2
- package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
- package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
- package/src/adapters/openai-responses/passthrough.ts +57 -125
- package/src/adapters/openai-responses/request-strips.ts +25 -3
- package/src/adapters/openai-responses/transport.ts +81 -0
- package/src/adapters/qoder/adapter.ts +4 -2
- package/src/adapters/registry.ts +5 -0
- package/src/adapters/zed.ts +26 -1
- package/src/advisor/activity.ts +55 -0
- package/src/advisor/budget.ts +62 -0
- package/src/advisor/consult.ts +4 -1
- package/src/advisor/context.ts +16 -4
- package/src/advisor/evidence-tool.ts +21 -0
- package/src/advisor/evidence.ts +117 -0
- package/src/advisor/runtime.ts +79 -31
- package/src/advisor/settings.ts +11 -2
- package/src/bridge/errors.ts +19 -1
- package/src/bridge/internal.ts +3 -1
- package/src/bridge/response-json.ts +18 -3
- package/src/bridge/sse.ts +58 -8
- package/src/bridge/terminal-refusal.ts +59 -0
- package/src/claude/desktop-3p-library.ts +32 -7
- package/src/claude/desktop-3p.ts +55 -13
- package/src/claude/desktop-first-party.ts +1 -1
- package/src/claude/desktop-picker-platform.ts +4 -0
- package/src/claude/desktop-picker.ts +15 -8
- package/src/claude/desktop-system-proxy.ts +50 -4
- package/src/claude/inbound-content-options.ts +11 -0
- package/src/claude/inbound.ts +33 -0
- package/src/claude/intercept/picker-ca-startup.ts +2 -1
- package/src/claude/intercept/picker-runtime.ts +6 -5
- package/src/claude/intercept/picker-trust-windows.ts +338 -0
- package/src/claude/intercept/picker-trust.ts +19 -0
- package/src/claude/intercept/port-policy.ts +30 -0
- package/src/claude/intercept/runtime.ts +97 -94
- package/src/claude/outbound.ts +38 -13
- package/src/cli/account-api.ts +1 -0
- package/src/cli/account-policy-dto.ts +6 -0
- package/src/cli/account-policy.ts +34 -5
- package/src/cli/account-preservation.ts +44 -0
- package/src/cli/account.ts +8 -2
- package/src/cli/advisor.ts +17 -3
- package/src/cli/capabilities-accounts.ts +20 -1
- package/src/cli/capabilities-agents-routing.ts +5 -2
- package/src/cli/capabilities-base.ts +38 -10
- package/src/cli/capabilities-observe-system.ts +63 -3
- package/src/cli/capabilities-provider-models.ts +11 -0
- package/src/cli/codex-cli-update.ts +151 -41
- package/src/cli/codex-provider.ts +44 -0
- package/src/cli/combo-input.ts +10 -1
- package/src/cli/combo-result.ts +1 -1
- package/src/cli/combo.ts +2 -1
- package/src/cli/cross-home-owner.ts +17 -14
- package/src/cli/dispatch.ts +25 -4
- package/src/cli/doctor.ts +6 -1
- package/src/cli/ensure-desktop-cold-start.ts +60 -7
- package/src/cli/help.ts +3 -2
- package/src/cli/index.ts +1 -1
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/log-history.ts +172 -0
- package/src/cli/models-runtime-subcommands.ts +1 -0
- package/src/cli/models-runtime.ts +72 -8
- package/src/cli/observe-stream.ts +2 -0
- package/src/cli/observe.ts +16 -4
- package/src/cli/provider.ts +27 -5
- package/src/cli/registry.ts +28 -8
- package/src/cli/runtime-api.ts +5 -3
- package/src/cli/system-command.ts +40 -8
- package/src/cli/uninstall-client-state.ts +32 -0
- package/src/cli/usage-compare.ts +309 -0
- package/src/cli/usage-model-search.ts +4 -4
- package/src/cli/usage-report.ts +2 -2
- package/src/cli/v2.ts +1 -1
- package/src/client/machine-listener.ts +1 -3
- package/src/client/runtime.ts +1 -0
- package/src/clients/client-path.ts +51 -0
- package/src/clients/config-export/contracts.ts +2 -1
- package/src/clients/config-export.ts +18 -44
- package/src/clients/gentle-shell-path.ts +87 -0
- package/src/codex/account-auto-switch.ts +2 -0
- package/src/codex/account-identity.ts +29 -0
- package/src/codex/account-label.ts +1 -1
- package/src/codex/account-lifecycle.ts +21 -5
- package/src/codex/account-preservation.ts +28 -0
- package/src/codex/account-use-remaining.ts +23 -0
- package/src/codex/app-server-processes.ts +97 -7
- package/src/codex/app-server-restart-service.ts +55 -4
- package/src/codex/auth-api/account-list.ts +4 -0
- package/src/codex/auth-api/account-pause-group.ts +6 -50
- package/src/codex/auth-api/account-preservation.ts +57 -0
- package/src/codex/auth-api/routes.ts +8 -0
- package/src/codex/auth-api/use-remaining-route.ts +36 -0
- package/src/codex/auth-api.ts +0 -1
- package/src/codex/auth-context.ts +2 -2
- package/src/codex/catalog/auto-review.ts +20 -3
- package/src/codex/catalog/build-entries.ts +9 -1
- package/src/codex/catalog/combo-member.ts +14 -5
- package/src/codex/catalog/configured-tiers.ts +1 -1
- package/src/codex/catalog/gather-capture.ts +2 -0
- package/src/codex/catalog/metadata.ts +11 -9
- package/src/codex/catalog/model-hints.ts +21 -16
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/parsing.ts +30 -86
- package/src/codex/catalog/provider-models.ts +36 -34
- package/src/codex/catalog/routed-gather.ts +12 -13
- package/src/codex/cli-install-provenance.ts +105 -17
- package/src/codex/cli-update-plan.ts +545 -0
- package/src/codex/config-write-lock.ts +23 -2
- package/src/codex/desktop-app/picker-freshness.ts +96 -0
- package/src/codex/desktop-app-restart.ts +21 -0
- package/src/codex/desktop-compatibility/installed-build.ts +24 -6
- package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
- package/src/codex/desktop-compatibility/runtime.ts +12 -3
- package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
- package/src/codex/features.ts +85 -45
- package/src/codex/inject/history-failure.ts +27 -0
- package/src/codex/inject/restore.ts +24 -2
- package/src/codex/inject/routing-classify.ts +4 -3
- package/src/codex/inject.ts +18 -27
- package/src/codex/main-account-hard-lock.ts +30 -1
- package/src/codex/native-profile-startup.ts +1 -1
- package/src/codex/ocx-compaction-history.ts +12 -10
- package/src/codex/paths.ts +86 -1
- package/src/codex/provider-routing.ts +174 -0
- package/src/codex/refresh.ts +2 -2
- package/src/codex/routing/health-store.ts +1 -1
- package/src/codex/routing/selection.ts +4 -3
- package/src/codex/routing/thread-affinity.ts +1 -1
- package/src/codex/signed-in-identity.ts +43 -0
- package/src/codex/subagent-model-fallback.ts +1 -1
- package/src/codex/sync.ts +6 -1
- package/src/codex/user-identity.ts +16 -5
- package/src/codex/windows-local-appdata.ts +84 -0
- package/src/combos/failover.ts +6 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +46 -10
- package/src/combos/types.ts +15 -0
- package/src/config/diagnostics.ts +33 -6
- package/src/config/live-reconcile.ts +38 -2
- package/src/config/load-degrade.ts +17 -0
- package/src/config/opencode-wire-upgrade.ts +54 -0
- package/src/config/opencode-zen-upgrade.ts +52 -0
- package/src/config/owner-registry.ts +22 -17
- package/src/config/provider-validation.ts +45 -2
- package/src/config/schema/config-schema.ts +22 -0
- package/src/config/schema/leaf-validators.ts +38 -9
- package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
- package/src/config/schema/native-reasoning-retention.ts +26 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +608 -284
- package/src/grok/sync.ts +1 -1
- package/src/images/loop.ts +46 -8
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +1 -0
- package/src/integrations/registry.ts +8 -0
- package/src/lib/codex-restart-contract.ts +15 -0
- package/src/lib/desktop-upstream-tunnel.ts +18 -53
- package/src/lib/empty-tool-catalog.ts +62 -0
- package/src/lib/errors.ts +19 -3
- package/src/lib/gcp-adc.ts +96 -9
- package/src/lib/gcp-credential-input.ts +12 -0
- package/src/lib/lab-live-route-production.ts +5 -0
- package/src/lib/opaque-proxy-tunnel.ts +217 -0
- package/src/lib/redact.ts +2 -2
- package/src/oauth/account-quota-rank.ts +69 -2
- package/src/oauth/anthropic-account-refusal.ts +20 -2
- package/src/oauth/anthropic-oauth-definitions.ts +3 -1
- package/src/oauth/anthropic-routing.ts +1 -21
- package/src/oauth/anthropic.ts +38 -7
- package/src/oauth/callback-server.ts +27 -1
- package/src/oauth/chatgpt-device.ts +1 -1
- package/src/oauth/chatgpt-shared.ts +83 -0
- package/src/oauth/chatgpt.ts +8 -72
- package/src/oauth/dsh.ts +203 -0
- package/src/oauth/generic-account-failover.ts +33 -7
- package/src/oauth/generic-session-affinity.ts +125 -0
- package/src/oauth/index.ts +184 -109
- package/src/oauth/local-token-detect.ts +21 -1
- package/src/oauth/login-cli-errors.ts +44 -0
- package/src/oauth/login-cli.ts +69 -7
- package/src/oauth/login-flow-state.ts +371 -23
- package/src/oauth/meta-muse-errors.ts +41 -0
- package/src/oauth/meta-muse-keychain.ts +52 -0
- package/src/oauth/meta-muse.ts +51 -84
- package/src/oauth/model-discovery-auth.ts +3 -0
- package/src/oauth/pool-settings-capability.ts +4 -2
- package/src/oauth/refresh-flight-retirement.ts +48 -0
- package/src/oauth/session-affinity-key.ts +23 -0
- package/src/oauth/store-anthropic-instance.ts +9 -2
- package/src/oauth/store.ts +12 -5
- package/src/protocols/encoders/adapter-events.ts +5 -4
- package/src/protocols/encoders/messages.ts +9 -6
- package/src/protocols/provider-summary.ts +2 -1
- package/src/providers/api-key-cooldowns.ts +49 -0
- package/src/providers/api-key-resolve.ts +11 -6
- package/src/providers/api-key-response-ownership.ts +57 -0
- package/src/providers/api-key-selection.ts +31 -6
- package/src/providers/api-keys.ts +214 -3
- package/src/providers/claude-cli-usage.ts +96 -0
- package/src/providers/dsh-account-destination.ts +31 -0
- package/src/providers/dsh-account-models.ts +41 -0
- package/src/providers/gcp-credential-cleanup.ts +59 -0
- package/src/providers/key-failover.ts +54 -50
- package/src/providers/key-store.ts +7 -2
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/model-rename-migration.ts +2 -2
- package/src/providers/openai-tiers-destination.ts +11 -2
- package/src/providers/openai-tiers.ts +5 -12
- package/src/providers/opencode-go-transport.ts +3 -1
- package/src/providers/quota/account-cache.ts +6 -5
- package/src/providers/quota/account-row.ts +34 -0
- package/src/providers/quota/antigravity.ts +28 -7
- package/src/providers/quota/dsh-account.ts +70 -0
- package/src/providers/quota/vendor-probes-oauth.ts +1 -93
- package/src/providers/quota-types.ts +2 -0
- package/src/providers/quota.ts +14 -20
- package/src/providers/registry/entries-core.ts +37 -8
- package/src/providers/registry/types.ts +1 -1
- package/src/providers/request-pacing.ts +77 -19
- package/src/providers/routed-config.ts +339 -0
- package/src/providers/service-tier.ts +2 -1
- package/src/quota/window-mapping.ts +5 -2
- package/src/responses/anthropic-progress.ts +34 -0
- package/src/responses/assistant-replay.ts +35 -0
- package/src/responses/compaction-history-framing.ts +65 -0
- package/src/responses/compaction.ts +6 -1
- package/src/responses/incomplete-reason.ts +31 -0
- package/src/responses/parser.ts +50 -5
- package/src/responses/reasoning-envelope.ts +4 -0
- package/src/responses/reasoning-replay-cache.ts +24 -2
- package/src/responses/reasoning-retention.ts +173 -0
- package/src/responses/retained-compaction.ts +46 -0
- package/src/responses/schema.ts +23 -0
- package/src/router.ts +8 -321
- package/src/routing/health.ts +37 -22
- package/src/routing/history/indexer.ts +6 -5
- package/src/routing/history/ingest.ts +6 -2
- package/src/routing/profile.ts +1 -1
- package/src/server/admission-model-scope.ts +17 -0
- package/src/server/auth-cors.ts +11 -0
- package/src/server/chat-completions.ts +7 -1
- package/src/server/chat-native-reasoning.ts +123 -0
- package/src/server/chat-native-sse.ts +110 -7
- package/src/server/chat-native.ts +76 -17
- package/src/server/claude-messages.ts +55 -414
- package/src/server/effort-policy.ts +1 -1
- package/src/server/images.ts +15 -2
- package/src/server/index/model-alias-id.ts +36 -0
- package/src/server/index/optional-listeners.ts +2 -0
- package/src/server/index/serve-options.ts +9 -1
- package/src/server/index/startup-warnings.ts +2 -0
- package/src/server/index.ts +1 -1
- package/src/server/inference/advisor-diagnostics.ts +31 -0
- package/src/server/management/advisor-routes.ts +15 -4
- package/src/server/management/claude-desktop-picker-routes.ts +1 -1
- package/src/server/management/combo-routes.ts +176 -50
- package/src/server/management/companion-routes.ts +1 -1
- package/src/server/management/config-routes.ts +75 -4
- package/src/server/management/cursor-integration-routes.ts +1 -1
- package/src/server/management/model-routes.ts +11 -6
- package/src/server/management/model-rows.ts +14 -0
- package/src/server/management/native-integration-routes.ts +2 -7
- package/src/server/management/native-reasoning-retention-routes.ts +41 -0
- package/src/server/management/oauth-account-routes.ts +87 -14
- package/src/server/management/provider-gcp-credential-routes.ts +29 -0
- package/src/server/management/provider-key-pacing-routes.ts +31 -0
- package/src/server/management/provider-overwrite-carry.ts +7 -0
- package/src/server/management/provider-routes.ts +25 -30
- package/src/server/management/route-registry.ts +10 -1
- package/src/server/management-api.ts +3 -1
- package/src/server/messages-native-tool-guard.ts +185 -0
- package/src/server/messages-native.ts +101 -21
- package/src/server/messages-shared.ts +431 -0
- package/src/server/relay.ts +1 -1
- package/src/server/request-log-terminal-status.ts +1 -1
- package/src/server/request-log.ts +6 -1
- package/src/server/responses/account-change-state.ts +8 -2
- package/src/server/responses/adapter-continuation.ts +13 -1
- package/src/server/responses/adapter-delivery.ts +19 -6
- package/src/server/responses/adapter-dispatch.ts +19 -2
- package/src/server/responses/advisor-retention.ts +117 -0
- package/src/server/responses/advisor-slot.ts +73 -41
- package/src/server/responses/codex-ws-wire.ts +15 -8
- package/src/server/responses/compact.ts +303 -125
- package/src/server/responses/compaction-recovery.ts +18 -10
- package/src/server/responses/compaction-retention.ts +61 -0
- package/src/server/responses/context-overflow.ts +11 -6
- package/src/server/responses/core-auth.ts +6 -0
- package/src/server/responses/core-codex-account.ts +11 -1
- package/src/server/responses/core-combo-failure.ts +2 -0
- package/src/server/responses/core-combo.ts +120 -36
- package/src/server/responses/core-errors.ts +3 -3
- package/src/server/responses/core-lifetime.ts +5 -2
- package/src/server/responses/core-opaque-recovery.ts +35 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-pipeline.ts +141 -0
- package/src/server/responses/core-replay.ts +18 -2
- package/src/server/responses/core.ts +2 -115
- package/src/server/responses/fetch-helpers.ts +73 -19
- package/src/server/responses/main-hard-lock-routing.ts +109 -0
- package/src/server/responses/native-response-control.ts +4 -2
- package/src/server/responses/passthrough-delivery.ts +3 -1
- package/src/server/responses/passthrough-dispatch.ts +15 -13
- package/src/server/responses/request-prepare.ts +76 -12
- package/src/server/responses/request-sidecar-auth.ts +2 -2
- package/src/server/responses/request-transport.ts +104 -27
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +96 -30
- package/src/server/responses/sidecar-execution.ts +20 -2
- package/src/server/responses/terminal-guard.ts +2 -16
- package/src/service/auth-preflight.ts +71 -0
- package/src/service/cli.ts +4 -2
- package/src/service/definition.ts +97 -0
- package/src/service/guards.ts +1 -61
- package/src/service/health-sentinel-linux.ts +112 -0
- package/src/service/health-sentinel-main.ts +76 -57
- package/src/service/health.ts +5 -89
- package/src/service/launchd.ts +27 -7
- package/src/service/logging.ts +72 -9
- package/src/service/repair.ts +2 -1
- package/src/service/state.ts +7 -8
- package/src/service/systemd.ts +1 -1
- package/src/service/windows-guarded-manager.ts +2 -1
- package/src/service/windows-ops.ts +1 -1
- package/src/service/windows-scheduler.ts +2 -7
- package/src/service/windows-taskxml.ts +2 -2
- package/src/service/windows-wscript.ts +14 -0
- package/src/service.ts +4 -2
- package/src/sidecar/auth.ts +2 -0
- package/src/types/config.ts +64 -3
- package/src/types/opencode-go.ts +61 -0
- package/src/types/opencode-zen.ts +29 -0
- package/src/types/provider.ts +32 -6
- package/src/types/request.ts +23 -2
- package/src/types/wire.ts +29 -37
- package/src/types.ts +1 -0
- package/src/update/npm-invocation.mjs +12 -2
- package/src/usage/cost.ts +48 -12
- package/src/usage/expected-prices.ts +3 -0
- package/src/usage/ledger-retention.ts +2 -1
- package/src/usage/ledger-scanner.ts +6 -5
- package/src/usage/log.ts +17 -8
- package/src/usage/summary.ts +22 -9
- package/src/usage/user-cost-overlays.ts +14 -19
- package/src/usage/user-cost-validation.ts +38 -0
- package/src/vision/eligibility.ts +13 -0
- package/src/web-search/loop.ts +55 -5
- package/src/web-search/progress-stream.ts +4 -1
- package/src/web-search/run-turn-loop.ts +9 -1
- package/gui/dist/assets/App-BjlttVLy.js +0 -58
- package/gui/dist/assets/App-CSJvmjNs.css +0 -1
- package/gui/dist/assets/index-BB6uK6tU.css +0 -1
- package/gui/dist/assets/index-BMssP4k3.js +0 -94
- package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { getConfigDir } from "../../config/paths";
|
|
6
|
+
import { isTestHomeGuardArmed } from "../../lib/test-home-guard";
|
|
7
|
+
import { detectClaudeCodeTokenAsync } from "../../oauth/local-token-detect";
|
|
8
|
+
import { readAnthropicUsageQuota } from "../../providers/claude-cli-usage";
|
|
9
|
+
import type { ProviderQuota } from "../../providers/quota-types";
|
|
10
|
+
|
|
11
|
+
export type ClaudeAdmission = {
|
|
12
|
+
state: "available" | "exhausted" | "unknown";
|
|
13
|
+
checkedAt: number;
|
|
14
|
+
resetAt?: number;
|
|
15
|
+
message?: string;
|
|
16
|
+
};
|
|
17
|
+
type Snapshot = { identity: string; checkedAt: number; quota?: ProviderQuota; refusalUntil?: number };
|
|
18
|
+
type Identity = { key: string; access: string };
|
|
19
|
+
export type AdmissionDeps = {
|
|
20
|
+
now?: () => number;
|
|
21
|
+
/** Resolve the signed-in account. `fresh` bypasses the short in-memory identity cache. */
|
|
22
|
+
identity?: (fresh?: boolean) => Identity | null | Promise<Identity | null>;
|
|
23
|
+
probe?: (token: string) => Promise<ProviderQuota | null>;
|
|
24
|
+
statePath?: string;
|
|
25
|
+
};
|
|
26
|
+
const CACHE_MS = 60_000;
|
|
27
|
+
const inflight = new Map<string, Promise<Snapshot | null>>();
|
|
28
|
+
const memory = new Map<string, Snapshot>();
|
|
29
|
+
let identityCache: { scope: string; at: number; value: Promise<Identity | null> } | undefined;
|
|
30
|
+
|
|
31
|
+
const nonEmpty = (value: unknown): value is string => typeof value === "string" && value.trim().length > 0;
|
|
32
|
+
|
|
33
|
+
async function readLocalIdentity(): Promise<Identity | null> {
|
|
34
|
+
// A test process never reads the operator's real sign-in or spends it on a usage probe;
|
|
35
|
+
// tests that exercise admission inject `deps.identity`.
|
|
36
|
+
if (isTestHomeGuardArmed()) return null;
|
|
37
|
+
const credential = await detectClaudeCodeTokenAsync();
|
|
38
|
+
if (!credential || !nonEmpty(credential.access)) return null;
|
|
39
|
+
let account: unknown;
|
|
40
|
+
try {
|
|
41
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR?.trim();
|
|
42
|
+
const configFile = configDir ? join(configDir, ".claude.json") : join(homedir(), ".claude.json");
|
|
43
|
+
account = JSON.parse(readFileSync(configFile, "utf8"))?.oauthAccount?.accountUuid;
|
|
44
|
+
} catch { /* Token fingerprint is the fallback when CLI has no account metadata. */ }
|
|
45
|
+
const seed = nonEmpty(account) ? account : nonEmpty(credential.refresh) ? credential.refresh : undefined;
|
|
46
|
+
if (!seed) return null;
|
|
47
|
+
return { key: createHash("sha256").update(seed).digest("hex"), access: credential.access };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The signed-in Claude Code account, read through the CLI's own secure storage. Cached briefly so
|
|
52
|
+
* a burst of turns costs one Keychain lookup; an account switch is noticed within the cache window.
|
|
53
|
+
*/
|
|
54
|
+
function localIdentity(fresh = false): Promise<Identity | null> {
|
|
55
|
+
const scope = process.env.CLAUDE_CONFIG_DIR?.trim() ?? "";
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
if (!fresh && identityCache && identityCache.scope === scope && now - identityCache.at < CACHE_MS) return identityCache.value;
|
|
58
|
+
const value = readLocalIdentity().catch(() => null);
|
|
59
|
+
identityCache = { scope, at: now, value };
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
async function resolveIdentity(deps: AdmissionDeps, fresh = false): Promise<Identity | null> {
|
|
63
|
+
try { return await (deps.identity ?? localIdentity)(fresh); } catch { return null; }
|
|
64
|
+
}
|
|
65
|
+
function statePath(deps: AdmissionDeps): string {
|
|
66
|
+
return deps.statePath ?? join(getConfigDir(), "claude-usage-admission.json");
|
|
67
|
+
}
|
|
68
|
+
function load(path: string): Snapshot | undefined {
|
|
69
|
+
try {
|
|
70
|
+
const saved = JSON.parse(readFileSync(path, "utf8"));
|
|
71
|
+
if (typeof saved.identity !== "string" || !Number.isFinite(saved.checkedAt)) return;
|
|
72
|
+
if (saved.refusalUntil !== undefined && !finite(saved.refusalUntil)) return;
|
|
73
|
+
if (saved.quota !== undefined) {
|
|
74
|
+
const q = saved.quota;
|
|
75
|
+
if (!q || typeof q !== "object" || !finite(q.updatedAt)) return;
|
|
76
|
+
for (const field of ["fiveHourPercent", "fiveHourResetAt", "weeklyPercent", "weeklyResetAt"])
|
|
77
|
+
if (q[field] !== undefined && !finite(q[field])) return;
|
|
78
|
+
if (q.customWindows !== undefined && (!Array.isArray(q.customWindows) || !q.customWindows.every((w: unknown) => {
|
|
79
|
+
if (!w || typeof w !== "object") return false;
|
|
80
|
+
const row = w as Record<string, unknown>;
|
|
81
|
+
return typeof row.label === "string" && finite(row.percent) && (row.resetAt === undefined || finite(row.resetAt));
|
|
82
|
+
}))) return;
|
|
83
|
+
}
|
|
84
|
+
return saved;
|
|
85
|
+
} catch { /* Missing or malformed cache is unknown, never proof of capacity. */ }
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
function save(path: string, snapshot: Snapshot): void {
|
|
89
|
+
memory.set(path, snapshot);
|
|
90
|
+
try {
|
|
91
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
92
|
+
const temp = `${path}.${process.pid}.tmp`;
|
|
93
|
+
writeFileSync(temp, JSON.stringify(snapshot) + "\n", { mode: 0o600 });
|
|
94
|
+
renameSync(temp, path);
|
|
95
|
+
} catch { /* Memory still suppresses launches; no secrets or upstream text are persisted. */ }
|
|
96
|
+
}
|
|
97
|
+
function finite(value: unknown): value is number { return typeof value === "number" && Number.isFinite(value); }
|
|
98
|
+
/** Windows that gate this model: the plan windows plus custom windows in scope for its family. */
|
|
99
|
+
function applicableWindows(quota: ProviderQuota | undefined, model: string): { percent?: number; resetAt?: number }[] {
|
|
100
|
+
const family = /opus|sonnet|fable|haiku/i.exec(model)?.[0]?.toLowerCase();
|
|
101
|
+
return [
|
|
102
|
+
{ percent: quota?.fiveHourPercent, resetAt: quota?.fiveHourResetAt },
|
|
103
|
+
{ percent: quota?.weeklyPercent, resetAt: quota?.weeklyResetAt },
|
|
104
|
+
...(quota?.customWindows ?? []).filter(w => w.scope !== "model" || !family || w.label.toLowerCase() === family),
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
/** A reset time in the proxy host's own time zone (or the given one), with the zone named. */
|
|
108
|
+
export function formatClaudeReset(resetAt: number, timeZone?: string): string {
|
|
109
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
110
|
+
...(timeZone ? { timeZone } : {}), month: "short", day: "numeric", hour: "numeric", minute: "2-digit", timeZoneName: "short",
|
|
111
|
+
}).format(resetAt);
|
|
112
|
+
}
|
|
113
|
+
function full(w: { percent?: number }): boolean { return finite(w.percent) && w.percent >= 100; }
|
|
114
|
+
function admission(snapshot: Snapshot, model: string, now: number): ClaudeAdmission {
|
|
115
|
+
const quota = snapshot.quota;
|
|
116
|
+
const exhausted = applicableWindows(quota, model).filter(w => full(w) && (!finite(w.resetAt) || w.resetAt > now));
|
|
117
|
+
if (finite(snapshot.refusalUntil) && snapshot.refusalUntil > now) exhausted.push({ percent: 100, resetAt: snapshot.refusalUntil });
|
|
118
|
+
if (exhausted.length) {
|
|
119
|
+
const resets = exhausted.map(w => w.resetAt).filter(finite);
|
|
120
|
+
// Unknown reset windows require another read after the short cache, not an invented reset date.
|
|
121
|
+
const resetAt = resets.length === exhausted.length ? Math.max(...resets) : Math.max(snapshot.checkedAt + CACHE_MS, ...resets);
|
|
122
|
+
return { state: "exhausted", checkedAt: snapshot.checkedAt, resetAt, message: `Claude subscription limits are exhausted. Claude launches are paused until ${formatClaudeReset(resetAt)}, then usage will be checked again.` };
|
|
123
|
+
}
|
|
124
|
+
return { state: quota ? "available" : "unknown", checkedAt: snapshot.checkedAt };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Read-only quota preflight for a `claude-cli` launch. Fails open: any local or upstream failure
|
|
129
|
+
* reports `unknown`, which admits the turn and lets the CLI report its own limit.
|
|
130
|
+
*/
|
|
131
|
+
export async function checkClaudeUsageAdmission(model = "", deps: AdmissionDeps = {}): Promise<ClaudeAdmission> {
|
|
132
|
+
const now = (deps.now ?? Date.now)();
|
|
133
|
+
try {
|
|
134
|
+
return await checkAdmission(model, deps, now);
|
|
135
|
+
} catch {
|
|
136
|
+
return { state: "unknown", checkedAt: now };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function checkAdmission(model: string, deps: AdmissionDeps, now: number): Promise<ClaudeAdmission> {
|
|
141
|
+
const identity = await resolveIdentity(deps);
|
|
142
|
+
if (!identity) return { state: "unknown", checkedAt: now };
|
|
143
|
+
const path = statePath(deps);
|
|
144
|
+
const disk = load(path);
|
|
145
|
+
const local = memory.get(path);
|
|
146
|
+
const cached = [disk, local].filter((s): s is Snapshot => s?.identity === identity.key).sort((a, b) => b.checkedAt - a.checkedAt || (b.refusalUntil ?? 0) - (a.refusalUntil ?? 0))[0];
|
|
147
|
+
if (cached) {
|
|
148
|
+
const status = admission(cached, model, now);
|
|
149
|
+
if (status.state === "exhausted" && status.resetAt! > now) return status;
|
|
150
|
+
// Same windows and threshold as admission(): only a full window in scope for this model forces a re-read.
|
|
151
|
+
const expiredReset = [cached.refusalUntil, ...applicableWindows(cached.quota, model).filter(full).map(w => w.resetAt)]
|
|
152
|
+
.some(reset => finite(reset) && reset <= now);
|
|
153
|
+
if (!expiredReset && now >= cached.checkedAt && now - cached.checkedAt < CACHE_MS) return status;
|
|
154
|
+
}
|
|
155
|
+
const requestKey = `${path}:${identity.key}`;
|
|
156
|
+
let probe = inflight.get(requestKey);
|
|
157
|
+
if (!probe) {
|
|
158
|
+
probe = (async () => {
|
|
159
|
+
let quota: ProviderQuota | null = null;
|
|
160
|
+
try { quota = await (deps.probe ?? readAnthropicUsageQuota)(identity.access); } catch { /* Unavailable is unknown. */ }
|
|
161
|
+
// A login change during the probe makes this answer belong to no current account: neither
|
|
162
|
+
// persist it nor admit or refuse with it.
|
|
163
|
+
if ((await resolveIdentity(deps, true))?.key !== identity.key) return null;
|
|
164
|
+
// Merge the newest refusal at commit time. Nothing awaits between this read and the write,
|
|
165
|
+
// so a refusal the CLI reported while the probe was in flight is never overwritten.
|
|
166
|
+
const at = (deps.now ?? Date.now)();
|
|
167
|
+
const refusals = [load(path), memory.get(path)]
|
|
168
|
+
.filter((s): s is Snapshot => s?.identity === identity.key)
|
|
169
|
+
.map(s => s.refusalUntil)
|
|
170
|
+
.filter((until): until is number => finite(until) && until > at);
|
|
171
|
+
const refusalUntil = refusals.length ? Math.max(...refusals) : undefined;
|
|
172
|
+
const snapshot: Snapshot = { identity: identity.key, checkedAt: at, ...(quota ? { quota } : {}), ...(refusalUntil ? { refusalUntil } : {}) };
|
|
173
|
+
save(path, snapshot);
|
|
174
|
+
return snapshot;
|
|
175
|
+
})().finally(() => inflight.delete(requestKey));
|
|
176
|
+
inflight.set(requestKey, probe);
|
|
177
|
+
}
|
|
178
|
+
const snapshot = await probe;
|
|
179
|
+
if (!snapshot) return { state: "unknown", checkedAt: (deps.now ?? Date.now)() };
|
|
180
|
+
return admission(snapshot, model, (deps.now ?? Date.now)());
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let preflightOverride: ((model: string) => Promise<ClaudeAdmission>) | undefined;
|
|
184
|
+
/** Test seam for the pre-dispatch check in the Responses executor; call with no argument to restore. */
|
|
185
|
+
export function setClaudeUsagePreflightForTests(fn?: (model: string) => Promise<ClaudeAdmission>): void { preflightOverride = fn; }
|
|
186
|
+
/** The pre-dispatch check the Responses executor runs before a claude-cli turn. */
|
|
187
|
+
export function claudeUsagePreflight(model: string): Promise<ClaudeAdmission> {
|
|
188
|
+
return (preflightOverride ?? checkClaudeUsageAdmission)(model);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Parse Claude's explicit next-clock reset, including its IANA timezone. */
|
|
192
|
+
export function parseClaudeReset(message: string, now = Date.now()): number | undefined {
|
|
193
|
+
if (!/hit.*limit|usage limit|rate limit/i.test(message)) return;
|
|
194
|
+
// Claude prints "resets 3pm (Europe/Paris)" or "resets 3:30pm (...)"; minutes are optional.
|
|
195
|
+
const match = /resets?\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)\s*\(([^)]+)\)/i.exec(message);
|
|
196
|
+
if (!match) return;
|
|
197
|
+
const hour = Number(match[1]); const minute = Number(match[2] ?? 0);
|
|
198
|
+
if (hour < 1 || hour > 12 || minute > 59) return;
|
|
199
|
+
const hours = hour % 12 + (match[3]!.toLowerCase() === "pm" ? 12 : 0);
|
|
200
|
+
try {
|
|
201
|
+
const formatter = new Intl.DateTimeFormat("en-US", { timeZone: match[4], year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hourCycle: "h23" });
|
|
202
|
+
const parts = (time: number) => Object.fromEntries(formatter.formatToParts(time).filter(p => p.type !== "literal").map(p => [p.type, Number(p.value)]));
|
|
203
|
+
const date = parts(now);
|
|
204
|
+
for (let day = 0; day < 2; day++) {
|
|
205
|
+
const target = Date.UTC(date.year!, date.month! - 1, date.day! + day, hours, minute);
|
|
206
|
+
let epoch = target;
|
|
207
|
+
for (let i = 0; i < 3; i++) {
|
|
208
|
+
const p = parts(epoch);
|
|
209
|
+
epoch += target - Date.UTC(p.year!, p.month! - 1, p.day!, p.hour!, p.minute!, p.second!);
|
|
210
|
+
}
|
|
211
|
+
if (epoch > now && epoch - now <= 26 * 60 * 60_000) return epoch;
|
|
212
|
+
}
|
|
213
|
+
} catch { /* An invalid or unsupported timezone never establishes a cooldown. */ }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Remember a limit the CLI itself reported ("You've hit your limit · resets 3pm (Europe/Paris)")
|
|
218
|
+
* so later launches are refused locally until that reset. Only a parsed reset records anything.
|
|
219
|
+
*/
|
|
220
|
+
export async function recordClaudeUsageRefusal(message: string, deps: AdmissionDeps = {}): Promise<void> {
|
|
221
|
+
const now = (deps.now ?? Date.now)();
|
|
222
|
+
const until = parseClaudeReset(message, now);
|
|
223
|
+
if (!until) return;
|
|
224
|
+
const identity = await resolveIdentity(deps);
|
|
225
|
+
if (!identity) return;
|
|
226
|
+
const path = statePath(deps);
|
|
227
|
+
const old = load(path);
|
|
228
|
+
const same = old?.identity === identity.key ? old : undefined;
|
|
229
|
+
save(path, { identity: identity.key, checkedAt: now, ...(same?.quota ? { quota: same.quota } : {}), refusalUntil: Math.max(until, same?.refusalUntil ?? 0) });
|
|
230
|
+
}
|
|
@@ -725,7 +725,7 @@ function imagePart(imageUrl: string): WireContentPart | undefined {
|
|
|
725
725
|
return undefined;
|
|
726
726
|
}
|
|
727
727
|
|
|
728
|
-
function formatMessageForHistory(message: OcxMessage, vendorNameByWire?: ReadonlyMap<string, string>): string {
|
|
728
|
+
export function formatMessageForHistory(message: OcxMessage, vendorNameByWire?: ReadonlyMap<string, string>): string {
|
|
729
729
|
if (message.role === "user") {
|
|
730
730
|
const text = typeof message.content === "string"
|
|
731
731
|
? message.content
|
|
@@ -133,6 +133,8 @@ export interface CodingAgentTurnInput {
|
|
|
133
133
|
toolBridge?: CodingAgentToolBridgeInput;
|
|
134
134
|
/** Vendor semantics and recovery permission stay with the adapter. */
|
|
135
135
|
classifyFailure?: (failure: CodingAgentFailure) => Extract<AdapterEvent, { type: "error" }>;
|
|
136
|
+
/** Family-specific replay projection; other coding-agent providers keep their existing layout. */
|
|
137
|
+
conversationInput?: typeof buildConversationInput;
|
|
136
138
|
deps: CodingAgentDeps;
|
|
137
139
|
}
|
|
138
140
|
|
|
@@ -146,6 +148,9 @@ export interface CodingAgentToolBridgeInput {
|
|
|
146
148
|
tools: ReadonlyArray<{ name: string; description: string; inputSchema: Record<string, unknown> }>;
|
|
147
149
|
/** CLI-emitted tool name (`mcp__<server>__<tool>`) to the request's wire tool name. */
|
|
148
150
|
emittedNameMap: Map<string, string>;
|
|
151
|
+
/** Claude replay may emit an exact selected wire name instead of its MCP transport name.
|
|
152
|
+
* These aliases are output-only: they never enter the CLI's --allowedTools list. */
|
|
153
|
+
acceptWireToolNames?: boolean;
|
|
149
154
|
/** Tool_use blocks accepted in one assistant message. */
|
|
150
155
|
maxTurnToolCalls: number;
|
|
151
156
|
/** Render prior tool calls with the CLI-emitted names. Off by default so CodeBuddy history is unchanged. */
|
|
@@ -237,6 +242,9 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
237
242
|
}
|
|
238
243
|
|
|
239
244
|
const toolBridge = input.toolBridge;
|
|
245
|
+
const wireToolNames = toolBridge?.acceptWireToolNames
|
|
246
|
+
? new Set(toolBridge.emittedNameMap.values())
|
|
247
|
+
: undefined;
|
|
240
248
|
const writeToolBridgeFile = deps.writeToolBridgeFile ?? writeFile;
|
|
241
249
|
let toolBridgeDir: string | undefined;
|
|
242
250
|
let toolBridgeMcpConfigPath: string | undefined;
|
|
@@ -304,12 +312,19 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
304
312
|
let producer: { close(): void } | undefined;
|
|
305
313
|
let permit: SingleUseDispatchPermit | undefined;
|
|
306
314
|
try {
|
|
315
|
+
if (incoming.abortSignal?.aborted) {
|
|
316
|
+
emit({ type: "error", message: "Coding-agent turn was aborted before start.", retryable: false });
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
307
319
|
producer = incoming.sendBudget?.beginSpendProducer?.();
|
|
308
320
|
const decision = incoming.sendBudget?.reserveDispatch({
|
|
309
321
|
sendClass: "initial", targetKey: `${provider.adapter}|${profile.canonicalBaseUrl}|${parsed.modelId}`,
|
|
310
322
|
});
|
|
311
323
|
if (decision && !decision.allowed) throw new SendBudgetExhaustedError();
|
|
312
324
|
permit = decision?.allowed ? decision.permit : undefined;
|
|
325
|
+
// Setup above awaits private catalog writes. Fence the actual CLI dispatch, outside
|
|
326
|
+
// spawn-error mapping so the selection owner can rebuild a refused turn.
|
|
327
|
+
incoming.beforePhysicalSend?.();
|
|
313
328
|
if (permit && !permit.use()) throw new SendBudgetExhaustedError();
|
|
314
329
|
incoming.onPhysicalSend?.({ ordinal: 1 });
|
|
315
330
|
let child: ChildProcess;
|
|
@@ -451,7 +466,7 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
451
466
|
const vendorNameByWire = toolBridge?.projectHistoryToolNames
|
|
452
467
|
? new Map([...toolBridge.emittedNameMap].map(([emitted, wire]) => [wire, emitted]))
|
|
453
468
|
: undefined;
|
|
454
|
-
for (const line of buildConversationInput(parsed, { maxHistoryChars: historyCharLimit, vendorNameByWire })) stdin.write(`${line}\n`);
|
|
469
|
+
for (const line of (input.conversationInput ?? buildConversationInput)(parsed, { maxHistoryChars: historyCharLimit, vendorNameByWire })) stdin.write(`${line}\n`);
|
|
455
470
|
stdin.end();
|
|
456
471
|
}
|
|
457
472
|
const stdout = child.stdout;
|
|
@@ -549,7 +564,8 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
549
564
|
break;
|
|
550
565
|
}
|
|
551
566
|
if (toolBridge && event.type === "tool_call_start") {
|
|
552
|
-
const wireName = toolBridge.emittedNameMap.get(event.name)
|
|
567
|
+
const wireName = toolBridge.emittedNameMap.get(event.name)
|
|
568
|
+
?? (wireToolNames?.has(event.name) ? event.name : undefined);
|
|
553
569
|
if (wireName === undefined) {
|
|
554
570
|
emitOnce({
|
|
555
571
|
type: "error",
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { IncomingMeta, ProviderAdapter } from "./base";
|
|
2
|
+
import { createAnthropicAdapter } from "./anthropic";
|
|
3
|
+
import type { AdapterFactoryContext } from "./registry";
|
|
4
|
+
import type { OcxParsedRequest, OcxProviderConfig } from "../types";
|
|
5
|
+
import { resolveDshAccountMessagesUrl } from "../providers/dsh-account-destination";
|
|
6
|
+
|
|
7
|
+
export { DSH_ACCOUNT_DEFAULT_MESSAGES_URL, resolveDshAccountMessagesUrl } from "../providers/dsh-account-destination";
|
|
8
|
+
|
|
9
|
+
export const DSH_ACCOUNT_RESERVED_HEADERS = new Set([
|
|
10
|
+
"x-dsh-auth-token",
|
|
11
|
+
"anthropic-version",
|
|
12
|
+
"authorization",
|
|
13
|
+
"x-api-key",
|
|
14
|
+
"host",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
export function createDshAccountAdapter(
|
|
18
|
+
provider: OcxProviderConfig,
|
|
19
|
+
_context?: AdapterFactoryContext,
|
|
20
|
+
): ProviderAdapter {
|
|
21
|
+
// Use key authMode internally so that createAnthropicAdapter doesn't inject Claude Code headers/prompt.
|
|
22
|
+
const baseAdapter = createAnthropicAdapter({
|
|
23
|
+
...provider,
|
|
24
|
+
authMode: "key",
|
|
25
|
+
}, "none");
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
...baseAdapter,
|
|
29
|
+
name: "dsh-account",
|
|
30
|
+
|
|
31
|
+
formatErrorBody(status: number, headers: Headers, payloadText: string): string {
|
|
32
|
+
if (status === 401) {
|
|
33
|
+
return "DeepSeek Harness account authentication failed (401 / ACCOUNT_TOKEN_INVALID). Please sign in to DeepSeek Harness Desktop and re-import.";
|
|
34
|
+
}
|
|
35
|
+
return baseAdapter.formatErrorBody?.(status, headers, payloadText) ?? payloadText;
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
async buildRequest(parsed: OcxParsedRequest, incoming: IncomingMeta) {
|
|
39
|
+
const token = provider.apiKey?.trim();
|
|
40
|
+
if (!token) {
|
|
41
|
+
throw new Error("DeepSeek Harness account token missing — import your account in Providers > DeepSeek Account (DSH)");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Fail-closed if custom provider configuration attempts to set any reserved header
|
|
45
|
+
if (provider.headers) {
|
|
46
|
+
for (const key of Object.keys(provider.headers)) {
|
|
47
|
+
if (DSH_ACCOUNT_RESERVED_HEADERS.has(key.toLowerCase())) {
|
|
48
|
+
throw new Error(`Reserved header "${key}" cannot be overridden in dsh-account provider configuration`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const req = await baseAdapter.buildRequest(parsed, incoming);
|
|
54
|
+
const url = resolveDshAccountMessagesUrl(provider.baseUrl);
|
|
55
|
+
|
|
56
|
+
const headers: Record<string, string> = { ...req.headers };
|
|
57
|
+
|
|
58
|
+
// Apply custom provider headers strictly before authoritative authentication headers
|
|
59
|
+
if (provider.headers) {
|
|
60
|
+
for (const [key, value] of Object.entries(provider.headers)) {
|
|
61
|
+
if (!DSH_ACCOUNT_RESERVED_HEADERS.has(key.toLowerCase()) && typeof value === "string") {
|
|
62
|
+
headers[key] = value;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Authoritative cleanup: strip any API key or Claude headers
|
|
68
|
+
for (const key of Object.keys(headers)) {
|
|
69
|
+
const lower = key.toLowerCase();
|
|
70
|
+
if (
|
|
71
|
+
lower === "authorization"
|
|
72
|
+
|| lower === "x-api-key"
|
|
73
|
+
|| lower === "x-dsh-auth-token"
|
|
74
|
+
|| lower === "anthropic-version"
|
|
75
|
+
|| lower === "anthropic-beta"
|
|
76
|
+
|| lower === "x-claude-code-session-id"
|
|
77
|
+
|| lower === "x-client-request-id"
|
|
78
|
+
) {
|
|
79
|
+
delete headers[key];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Set authoritative DeepSeek Harness Account headers strictly at the end
|
|
84
|
+
headers["x-dsh-auth-token"] = token;
|
|
85
|
+
headers["anthropic-version"] = "2023-06-01";
|
|
86
|
+
if (!headers["Content-Type"]) headers["Content-Type"] = "application/json";
|
|
87
|
+
if (!headers["Accept"]) headers["Accept"] = parsed.stream ? "text/event-stream" : "application/json";
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
...req,
|
|
91
|
+
url,
|
|
92
|
+
headers,
|
|
93
|
+
redirect: "manual",
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
package/src/adapters/google.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { openCodeZenGeminiEndpoint } from "../types/opencode-zen";
|
|
1
2
|
import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base";
|
|
2
3
|
import { debugDroppedFrame, debugProviderDiagnosticLazy } from "../lib/debug";
|
|
3
4
|
import { createToolCallIdAllocator } from "./tool-call-id";
|
|
5
|
+
import { openCodeGoGeminiEndpoint } from "../types/opencode-go";
|
|
4
6
|
import { createImageBudget, materializeInlineImage, MAX_ENCODED_BYTES_PER_IMAGE, artifactHttpUrl } from "../images/artifacts";
|
|
5
7
|
import type {
|
|
6
8
|
AdapterEvent,
|
|
@@ -17,7 +19,7 @@ import type {
|
|
|
17
19
|
import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolChoiceToolPredicate } from "../types";
|
|
18
20
|
import type { OcxTool } from "../types";
|
|
19
21
|
import { contentPartsToText, parseDataUrl } from "./image";
|
|
20
|
-
import { getVertexAccessToken } from "../lib/gcp-adc";
|
|
22
|
+
import { GCP_CREDENTIAL_MARKER_PREFIX, gcpCredentialMarkerAccount, getVertexAccessToken } from "../lib/gcp-adc";
|
|
21
23
|
import { fetchAntigravityWithRetry, fetchVertexWithRetry } from "./google-http";
|
|
22
24
|
import { safeAntigravityHttpErrorMessage, safeVertexHttpErrorMessage } from "./google-errors";
|
|
23
25
|
import { isVertexTruncatedTurn, vertexTruncationErrorEvent } from "./google-truncation";
|
|
@@ -174,7 +176,15 @@ function resolveDirectGeminiWireModelId(modelId: string, applyRenames: boolean):
|
|
|
174
176
|
|
|
175
177
|
/** Vertex API key: provider.apiKey if it looks real (not a sentinel), else GOOGLE_CLOUD_API_KEY env. */
|
|
176
178
|
function resolveVertexApiKey(optKey?: string): string | undefined {
|
|
177
|
-
|
|
179
|
+
// A `gcp-sa:` marker is the keychain-backed credential reference (see gcp-adc): it is not
|
|
180
|
+
// API-key material, so it must not be sent as x-goog-api-key — the ADC branch below handles it.
|
|
181
|
+
// AND the marker means the operator named one credential explicitly: the GOOGLE_CLOUD_API_KEY
|
|
182
|
+
// env fallback must NOT fire (it would silently route this provider's inference through the
|
|
183
|
+
// ambient env key instead of the configured service account).
|
|
184
|
+
if (optKey && optKey.startsWith(GCP_CREDENTIAL_MARKER_PREFIX)) return undefined;
|
|
185
|
+
const realKey = optKey && !optKey.startsWith("<") && optKey !== "N/A"
|
|
186
|
+
? optKey
|
|
187
|
+
: undefined;
|
|
178
188
|
return realKey || process.env.GOOGLE_CLOUD_API_KEY;
|
|
179
189
|
}
|
|
180
190
|
|
|
@@ -864,6 +874,7 @@ function invalidGoogleShapeEvent(
|
|
|
864
874
|
};
|
|
865
875
|
}
|
|
866
876
|
|
|
877
|
+
/** Build Gemini transport and response parsing for AI Studio, Vertex, Cloud Code Assist or Go. */
|
|
867
878
|
export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapter {
|
|
868
879
|
const toolSchemaPolicy = provider.googleToolSchemaPolicy ?? "compatible";
|
|
869
880
|
const toolSchemaProfile = {
|
|
@@ -945,6 +956,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
945
956
|
}
|
|
946
957
|
: {}),
|
|
947
958
|
|
|
959
|
+
/** Compile an admitted request into the selected Gemini endpoint, body and authentication. */
|
|
948
960
|
async buildRequest(parsed: OcxParsedRequest) {
|
|
949
961
|
// Structured-output admission runs FIRST, before messagesToGeminiFormat writes
|
|
950
962
|
// lastInjectedCallIds/lastReasoningReplayScope: a refused request must not leave
|
|
@@ -983,7 +995,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
983
995
|
mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning),
|
|
984
996
|
provider.baseUrl,
|
|
985
997
|
).wireModelId
|
|
986
|
-
: provider.googleMode === "vertex"
|
|
998
|
+
: provider.googleMode === "vertex" || openCodeZenGeminiEndpoint(provider, parsed.modelId) !== undefined
|
|
987
999
|
? parsed.modelId
|
|
988
1000
|
: resolveDirectGeminiWireModelId(parsed.modelId, provider.directGeminiWireRenames !== false);
|
|
989
1001
|
returnsThoughtSummaries = provider.googleMode === "cloud-code-assist"
|
|
@@ -1197,16 +1209,31 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
1197
1209
|
if (locationError) throw new Error(locationError);
|
|
1198
1210
|
const host = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
|
|
1199
1211
|
const url = `https://${host}/v1/projects/${project}/locations/${location}/publishers/google/models/${parsed.modelId}:${method}${streamParam}`;
|
|
1200
|
-
|
|
1212
|
+
// The marker account travels per-request as an argument (never process-global state): a
|
|
1213
|
+
// module-level "active marker" written at routing time would race across concurrent
|
|
1214
|
+
// requests for different providers and could resolve one call with another's credential.
|
|
1215
|
+
const markerAccount = gcpCredentialMarkerAccount(provider.apiKey);
|
|
1216
|
+
const token = await getVertexAccessToken({ markerAccount });
|
|
1201
1217
|
headers["Authorization"] = `Bearer ${token}`;
|
|
1202
1218
|
return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
|
|
1203
1219
|
}
|
|
1204
1220
|
|
|
1205
|
-
//
|
|
1206
|
-
const
|
|
1221
|
+
// Go and Zen use separate guarded Gemini endpoints; AI Studio retains /v1beta.
|
|
1222
|
+
const goEndpoint = openCodeGoGeminiEndpoint(provider, routedModelId);
|
|
1223
|
+
const zenEndpoint = openCodeZenGeminiEndpoint(provider, routedModelId);
|
|
1224
|
+
const endpoint = goEndpoint ?? zenEndpoint ?? `${provider.baseUrl}/v1beta/models/${routedModelId}`;
|
|
1225
|
+
const url = `${endpoint}:${method}${streamParam}`;
|
|
1207
1226
|
const apiKey = provider.apiKey?.trim();
|
|
1208
|
-
if (!apiKey) throw new Error("google (AI Studio) requires a non-empty API key
|
|
1209
|
-
|
|
1227
|
+
if (!apiKey) throw new Error(`${goEndpoint ? "OpenCode Go" : zenEndpoint ? "OpenCode Zen" : "google (AI Studio)"} requires a non-empty API key`);
|
|
1228
|
+
if (goEndpoint) {
|
|
1229
|
+
// Console inference documents Bearer auth. Send only the selected Go key.
|
|
1230
|
+
for (const name of Object.keys(headers)) {
|
|
1231
|
+
if (["authorization", "x-goog-api-key", "x-api-key"].includes(name.toLowerCase())) delete headers[name];
|
|
1232
|
+
}
|
|
1233
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
1234
|
+
} else {
|
|
1235
|
+
headers["x-goog-api-key"] = apiKey;
|
|
1236
|
+
}
|
|
1210
1237
|
|
|
1211
1238
|
const compiled = compileGoogleWireBody(body, toolSchemaProfile, toolSchemaPolicy);
|
|
1212
1239
|
reportToolSchemaLoss(compiled.toolSchemaLossReport);
|
|
@@ -7,6 +7,7 @@ import { EMPTY_TOOL_OUTPUT_ANNOTATION, isWhitespaceOnlyTextPartArray } from "../
|
|
|
7
7
|
import { identifyRoutedModel } from "../identity";
|
|
8
8
|
import { buildNonOpenAIToolCatalogNudgeForTools, shouldInjectNonOpenAIToolCatalogNudge } from "../tool-catalog-nudge";
|
|
9
9
|
import { peekReasoningForCall } from "../../responses/reasoning-replay-cache";
|
|
10
|
+
import { chatReasoningReplayPolicy } from "./reasoning-replay";
|
|
10
11
|
import type { ClaudeThinkingProjection } from "../../lib/claude-request-projection";
|
|
11
12
|
import { inlineDocumentDataUrl } from "../../responses/inline-document";
|
|
12
13
|
import type { OcxAssistantMessage, OcxContentPart, OcxParsedRequest, OcxProviderConfig, OcxTextContent, OcxThinkingContent, OcxToolCall } from "../../types";
|
|
@@ -75,7 +76,7 @@ export function openAIChatSerializesThinking(
|
|
|
75
76
|
modelId: string,
|
|
76
77
|
): ClaudeThinkingProjection {
|
|
77
78
|
return {
|
|
78
|
-
text:
|
|
79
|
+
text: chatReasoningReplayPolicy(provider, modelId).preserve,
|
|
79
80
|
// The wire has no `signature` field — base64 replay tokens are Anthropic-wire-only.
|
|
80
81
|
signature: false,
|
|
81
82
|
// A `redacted_thinking` block is opaque provider data with no Chat representation: the
|
|
@@ -267,7 +268,7 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
|
|
|
267
268
|
// recorded under every call id — join unique texts only.
|
|
268
269
|
if (cached.length > 0) {
|
|
269
270
|
reasoningContent = [...new Set(cached)].join("\n");
|
|
270
|
-
} else if (
|
|
271
|
+
} else if (chatReasoningReplayPolicy(provider, parsed.modelId).placeholder) {
|
|
271
272
|
// Fallback (extends #950 and #1193; fixes #5421): the replay cache is
|
|
272
273
|
// bounded (64 entries / 256 KiB / 1 h TTL) and always misses on
|
|
273
274
|
// long sessions, and some thinking/tool rounds carry no recorded
|
|
@@ -354,7 +355,7 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
|
|
|
354
355
|
const orphanReasoning =
|
|
355
356
|
cachedReasoning
|
|
356
357
|
|| (wireSerializesThinking
|
|
357
|
-
&&
|
|
358
|
+
&& chatReasoningReplayPolicy(provider, parsed.modelId).placeholder
|
|
358
359
|
? " "
|
|
359
360
|
: undefined);
|
|
360
361
|
const orphanReasoningFields: Record<string, unknown> = !orphanReasoning
|
|
@@ -14,6 +14,8 @@ import { modelRecordValue } from "../../reasoning-effort";
|
|
|
14
14
|
import { modelInList, type OcxProviderConfig } from "../../types";
|
|
15
15
|
import { chatParallelToolCallsWireValue } from "./parallel-tool-calls";
|
|
16
16
|
import { applyExplicitChatReasoningWirePolicy } from "./reasoning-wire";
|
|
17
|
+
import { replayNativeChatReasoning } from "./reasoning-replay";
|
|
18
|
+
import type { OcxReasoningReplayScopeRef } from "../../types";
|
|
17
19
|
|
|
18
20
|
const CHAT_PASSTHROUGH_FIELDS = [
|
|
19
21
|
"audio",
|
|
@@ -55,17 +57,19 @@ export function buildOpenAIChatPassthroughRequest(
|
|
|
55
57
|
fastPolicy: ResolvedFastPolicy = fastPolicyForModel(provider, modelId, undefined, "chat"),
|
|
56
58
|
fastMode?: boolean,
|
|
57
59
|
providerName?: string,
|
|
60
|
+
replayScope?: OcxReasoningReplayScopeRef,
|
|
58
61
|
): AdapterRequest {
|
|
59
62
|
const { url, headers, hasCredential } = openAIChatTransport(provider);
|
|
60
63
|
|
|
61
64
|
const body: Record<string, unknown> = {
|
|
62
65
|
model: provider.modelSuffixBracketStrip ? stripBracketedModelSuffix(modelId) : modelId,
|
|
63
|
-
// The caller's messages
|
|
66
|
+
// The caller's messages retain their wire shape. An operator who
|
|
64
67
|
// recorded that this destination rejects the `developer` role gets that role converted in
|
|
65
68
|
// place. Verbatim was not neutral there — it sent the role anyway and the turn failed
|
|
66
69
|
// upstream with a 400 before the model saw it. An unrecorded destination is still verbatim,
|
|
67
70
|
// and the conversion changes the role of those messages and nothing else, so the position
|
|
68
|
-
// of every message and every other field survive unchanged.
|
|
71
|
+
// of every message and every other field survive unchanged. Missing thinking history is
|
|
72
|
+
// repaired below using the same preserve policy as the translated Chat builder.
|
|
69
73
|
messages: applyExplicitChatDeveloperRole(
|
|
70
74
|
frameAgentRouterMessages(provider.baseUrl, rawBody.messages),
|
|
71
75
|
provider,
|
|
@@ -75,6 +79,8 @@ export function buildOpenAIChatPassthroughRequest(
|
|
|
75
79
|
for (const field of CHAT_PASSTHROUGH_FIELDS) {
|
|
76
80
|
if (rawBody[field] !== undefined) body[field] = rawBody[field];
|
|
77
81
|
}
|
|
82
|
+
body.messages = replayNativeChatReasoning(body.messages, provider, modelId, replayScope,
|
|
83
|
+
Array.isArray(rawBody.tools) && rawBody.tools.length > 0);
|
|
78
84
|
if (protectGlmSummaryBudget(body, provider.baseUrl, body.reasoning_effort)) body.reasoning_effort = "low";
|
|
79
85
|
const rawEfforts = modelRecordValue(provider.modelReasoningEfforts, modelId) ?? provider.reasoningEfforts;
|
|
80
86
|
const reasoningDisabled = modelInList(provider.noReasoningModels, modelId) || rawEfforts?.length === 0;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { modelInList, type OcxProviderConfig, type OcxReasoningReplayScopeRef } from "../../types";
|
|
2
|
+
import { peekReasoningForCall } from "../../responses/reasoning-replay-cache";
|
|
3
|
+
|
|
4
|
+
type Rec = Record<string, unknown>;
|
|
5
|
+
export const isChatRecord = (value: unknown): value is Rec =>
|
|
6
|
+
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
7
|
+
|
|
8
|
+
const isDeepSeekThinkingModel = (modelId: string): boolean =>
|
|
9
|
+
/(?:^|\/)deepseek-(?:v\d+(?:\.\d+)*-(?:flash|pro)|flash|pro)(?:$|[-_:])/i.test(modelId);
|
|
10
|
+
|
|
11
|
+
/** DeepSeek thinking tool replay is a model contract, independent of the gateway's name. */
|
|
12
|
+
export function chatReasoningReplayPolicy(provider: OcxProviderConfig, modelId: string): {
|
|
13
|
+
preserve: boolean; placeholder: boolean;
|
|
14
|
+
} {
|
|
15
|
+
// Match whole model-family tokens, including dotted versions, namespace and deployment suffixes.
|
|
16
|
+
// Legacy chat/reasoner and distilled R1 models do not share the thinking tool-call contract.
|
|
17
|
+
const preserve = provider.preserveReasoningContentModels === undefined
|
|
18
|
+
? isDeepSeekThinkingModel(modelId)
|
|
19
|
+
: modelInList(provider.preserveReasoningContentModels, modelId);
|
|
20
|
+
return {
|
|
21
|
+
preserve,
|
|
22
|
+
placeholder: preserve && (provider.requiresReasoningPlaceholderModels === undefined
|
|
23
|
+
? true
|
|
24
|
+
: modelInList(provider.requiresReasoningPlaceholderModels, modelId)),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Repair missing native history without mutating caller bodies used by retries/combo siblings. */
|
|
29
|
+
export function replayNativeChatReasoning(
|
|
30
|
+
messages: unknown,
|
|
31
|
+
provider: OcxProviderConfig,
|
|
32
|
+
modelId: string,
|
|
33
|
+
scope?: OcxReasoningReplayScopeRef,
|
|
34
|
+
hasTools = false,
|
|
35
|
+
): unknown {
|
|
36
|
+
const policy = chatReasoningReplayPolicy(provider, modelId);
|
|
37
|
+
if (!policy.preserve || !Array.isArray(messages)
|
|
38
|
+
|| modelInList(provider.reasoningDetailsModels, modelId)) return messages;
|
|
39
|
+
return messages.map(message => {
|
|
40
|
+
if (!isChatRecord(message) || message.role !== "assistant"
|
|
41
|
+
|| (typeof message.reasoning_content === "string" && message.reasoning_content.length > 0)) return message;
|
|
42
|
+
const calls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
43
|
+
// DeepSeek's tools contract covers all prior assistant turns, even plain answers.
|
|
44
|
+
// Those turns have no call id for original replay; only the configured fallback applies.
|
|
45
|
+
if (calls.length === 0 && !(hasTools && isDeepSeekThinkingModel(modelId))) return message;
|
|
46
|
+
const cached = calls.filter(isChatRecord)
|
|
47
|
+
.map(tool => typeof tool.id === "string" ? peekReasoningForCall(tool.id, scope) : undefined)
|
|
48
|
+
.filter((text): text is string => typeof text === "string" && text.length > 0);
|
|
49
|
+
const reasoning = cached.length > 0 ? [...new Set(cached)].join("\n") : policy.placeholder ? " " : undefined;
|
|
50
|
+
return reasoning === undefined ? message : { ...message, reasoning_content: reasoning };
|
|
51
|
+
});
|
|
52
|
+
}
|