@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
package/src/grok/sync.ts
CHANGED
|
@@ -18,7 +18,7 @@ export interface GrokSyncDeps {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
async function defaultFetchAllModels(config: OcxConfig): Promise<CatalogModel[]> {
|
|
21
|
-
const { fetchAllModels } = await import("../server/management
|
|
21
|
+
const { fetchAllModels } = await import("../server/management/shared");
|
|
22
22
|
return fetchAllModels(config);
|
|
23
23
|
}
|
|
24
24
|
|
package/src/images/loop.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { isAnthropicInstanceId } from "../providers/anthropic-instance-id";
|
|
|
20
20
|
import { cloneProviderOpaqueToolCallMetadata } from "../responses/provider-opaque-metadata";
|
|
21
21
|
import type { AttemptRecoveryKind } from "../usage/log";
|
|
22
22
|
import { bridgeToResponsesSSE } from "../bridge";
|
|
23
|
+
import { isTerminalRefusalIncomplete } from "../bridge/terminal-refusal";
|
|
23
24
|
import { clearableDeadline, idleDeadline } from "../lib/abort";
|
|
24
25
|
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
25
26
|
import { applyUpstreamRecoveryInit, fetchWithResetRetry, prepareSameTarget429Wait } from "../lib/upstream-retry";
|
|
@@ -39,6 +40,7 @@ import { submitVideoJob } from "./xai-video-client";
|
|
|
39
40
|
import { downloadVideoToArtifact, createImageBudget, pruneArtifacts } from "./artifacts";
|
|
40
41
|
import { IMAGE_GEN_TOOL_NAME, VIDEO_GEN_TOOL_NAME } from "./synthetic-tool";
|
|
41
42
|
import type { ImageBridgePlan, VideoBridgePlan } from "./types";
|
|
43
|
+
import { guardEmptyToolCatalogStream, type ClientToolCatalogRestriction } from "../lib/empty-tool-catalog";
|
|
42
44
|
|
|
43
45
|
const SSE_HEADERS = {
|
|
44
46
|
"Content-Type": "text/event-stream",
|
|
@@ -241,7 +243,11 @@ function extractIterationThinking(events: AdapterEvent[]): OcxThinkingContent[]
|
|
|
241
243
|
};
|
|
242
244
|
|
|
243
245
|
for (const e of events) {
|
|
244
|
-
if (e.type === "
|
|
246
|
+
if (e.type === "text_delta" && e.anthropicThinkingSignature) {
|
|
247
|
+
flushVisible();
|
|
248
|
+
flushRaw();
|
|
249
|
+
parts.push({ type: "thinking", thinking: e.text, signature: e.anthropicThinkingSignature });
|
|
250
|
+
} else if (e.type === "thinking_delta") {
|
|
245
251
|
flushRaw();
|
|
246
252
|
thinking += e.thinking;
|
|
247
253
|
} else if (e.type === "reasoning_raw_delta") {
|
|
@@ -286,6 +292,7 @@ class LoopError extends Error {
|
|
|
286
292
|
*/
|
|
287
293
|
export interface ImageBridgeDeps {
|
|
288
294
|
parsed: OcxParsedRequest;
|
|
295
|
+
clientToolCatalogRestriction?: ClientToolCatalogRestriction;
|
|
289
296
|
adapter: ProviderAdapter;
|
|
290
297
|
incomingMeta: IncomingMeta;
|
|
291
298
|
plan?: ImageBridgePlan;
|
|
@@ -319,6 +326,12 @@ export interface ImageBridgeDeps {
|
|
|
319
326
|
waitForRequestSlot?: (signal?: AbortSignal) => Promise<ProviderRequestSlot | void>;
|
|
320
327
|
/** Raw adapter usage at the terminal event, pre wire-normalization (see bridgeToResponsesSSE onUsage). */
|
|
321
328
|
onUsage?: (usage: OcxUsage | undefined) => void;
|
|
329
|
+
/**
|
|
330
|
+
* After an `on429` rotation, offered the usage of the hidden iterations the left credential
|
|
331
|
+
* served. Returning true means it was billed to that credential's attempt, so `onUsage` will not
|
|
332
|
+
* report it again; the client terminal still includes it.
|
|
333
|
+
*/
|
|
334
|
+
onRotatedUsage?: (usage: OcxUsage) => boolean;
|
|
322
335
|
/**
|
|
323
336
|
* Optional 429 failover for the routed (non-xAI) model. Return a rebuilt adapter for the
|
|
324
337
|
* rotated credential, or null when the pool is exhausted. Async hooks support OAuth refresh;
|
|
@@ -353,6 +366,8 @@ export interface ImageBridgeDeps {
|
|
|
353
366
|
onCompletedResponse?: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => void;
|
|
354
367
|
/** WebSocket Responses path only — leave response id empty for protocol compatibility. */
|
|
355
368
|
forceEmptyResponseId?: boolean;
|
|
369
|
+
/** Responses inbound only: deliver a non-retryable upstream refusal as `response.failed`. */
|
|
370
|
+
terminalRefusalAsFailed?: boolean;
|
|
356
371
|
}
|
|
357
372
|
|
|
358
373
|
/**
|
|
@@ -377,6 +392,10 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
377
392
|
let paidImageCalls = 0;
|
|
378
393
|
let paidVideoCalls = 0;
|
|
379
394
|
let hiddenUsage: OcxUsage | undefined;
|
|
395
|
+
// Hidden usage already billed through onRotatedUsage: still on the client terminal, not in
|
|
396
|
+
// ledgerUsage, which is what onUsage reports once a rotation has moved some.
|
|
397
|
+
let rotatedUsage: OcxUsage | undefined;
|
|
398
|
+
let ledgerUsage: OcxUsage | undefined;
|
|
380
399
|
let activeProducers = 0;
|
|
381
400
|
|
|
382
401
|
const addUsage = (a: OcxUsage | undefined, b: OcxUsage | undefined): OcxUsage | undefined => {
|
|
@@ -569,6 +588,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
569
588
|
// runTurn adapters signal errors via {type:"error"} events, not HTTP status codes.
|
|
570
589
|
const errorEvent = events.find(e => e.type === "error");
|
|
571
590
|
if (errorEvent && errorEvent.type === "error") {
|
|
591
|
+
hiddenUsage = addUsage(hiddenUsage, errorEvent.usage);
|
|
572
592
|
if (errorEvent.code === "translation_buffer_limit") {
|
|
573
593
|
throw new TranslatorBudgetExceededError("retained_collectors", TRANSLATOR_MAX_TURN_BYTES);
|
|
574
594
|
}
|
|
@@ -709,6 +729,10 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
709
729
|
const rotated = await deps.on429(prepared.response.headers.get("retry-after"), prepared.response.headers,
|
|
710
730
|
iterParsed, prepared.response);
|
|
711
731
|
if (!rotated) break;
|
|
732
|
+
if (hiddenUsage && deps.onRotatedUsage?.(hiddenUsage)) {
|
|
733
|
+
rotatedUsage = addUsage(rotatedUsage, hiddenUsage);
|
|
734
|
+
hiddenUsage = undefined;
|
|
735
|
+
}
|
|
712
736
|
try { void prepared.response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
|
|
713
737
|
adapter = rotated.adapter;
|
|
714
738
|
yield { type: "heartbeat" };
|
|
@@ -772,6 +796,8 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
772
796
|
signal,
|
|
773
797
|
inactivityTimeoutMs: stallTimeoutMs,
|
|
774
798
|
translatorBudget,
|
|
799
|
+
// A pass that fails was still billed; the error terminal below reports it with the hidden passes.
|
|
800
|
+
onErrorUsage: usage => { hiddenUsage = addUsage(hiddenUsage, usage); },
|
|
775
801
|
})) {
|
|
776
802
|
if (event.type === "heartbeat") yield event;
|
|
777
803
|
else {
|
|
@@ -863,13 +889,17 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
863
889
|
// Loop (fulfill + re-ask) ONLY when the model's actionable output is purely image_gen. A
|
|
864
890
|
// real tool call means this turn is terminal for Codex — finalize so those calls reach
|
|
865
891
|
// Codex. forceFinal also finalizes.
|
|
866
|
-
|
|
892
|
+
// A refused iteration also finalizes: fulfilling its media call would re-ask the model
|
|
893
|
+
// with the refused context (#7148).
|
|
894
|
+
const shouldLoop = split.calls.length > 0 && !split.hasRealToolCall && !forceFinal
|
|
895
|
+
&& !split.passthrough.some(isTerminalRefusalIncomplete);
|
|
867
896
|
if (!shouldLoop) {
|
|
868
|
-
if (hiddenUsage) {
|
|
897
|
+
if (hiddenUsage || rotatedUsage) {
|
|
869
898
|
for (let i = split.passthrough.length - 1; i >= 0; i--) {
|
|
870
899
|
const e = split.passthrough[i];
|
|
871
900
|
if (e?.type === "done" || e?.type === "incomplete") {
|
|
872
|
-
|
|
901
|
+
ledgerUsage = addUsage(hiddenUsage, e.usage);
|
|
902
|
+
split.passthrough[i] = { ...e, usage: addUsage(rotatedUsage, ledgerUsage) };
|
|
873
903
|
break;
|
|
874
904
|
}
|
|
875
905
|
}
|
|
@@ -881,6 +911,10 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
881
911
|
// Discarded iteration still contributed tokens — accumulate for the final onUsage.
|
|
882
912
|
takeUsageFrom(split.passthrough);
|
|
883
913
|
|
|
914
|
+
for (const event of split.passthrough) {
|
|
915
|
+
if (event.type === "text_delta" && event.anthropicThinkingSignature) yield event;
|
|
916
|
+
}
|
|
917
|
+
|
|
884
918
|
// Fulfill each image/video call, then inject ONE assistant turn (thinking once + all tool
|
|
885
919
|
// calls) so Anthropic extended-thinking continuations stay valid across parallel calls.
|
|
886
920
|
const iterationThinking = extractIterationThinking(split.passthrough);
|
|
@@ -1052,6 +1086,8 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
1052
1086
|
});
|
|
1053
1087
|
}
|
|
1054
1088
|
} catch (e) {
|
|
1089
|
+
ledgerUsage = hiddenUsage;
|
|
1090
|
+
const usage = addUsage(rotatedUsage, hiddenUsage);
|
|
1055
1091
|
if (isTranslatorBudgetExceededError(e)) {
|
|
1056
1092
|
yield {
|
|
1057
1093
|
type: "error",
|
|
@@ -1059,14 +1095,14 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
1059
1095
|
errorType: "upstream_error",
|
|
1060
1096
|
code: e.code,
|
|
1061
1097
|
message: "upstream translation buffer exceeded the safe limit",
|
|
1062
|
-
...(
|
|
1098
|
+
...(usage ? { usage } : {}),
|
|
1063
1099
|
};
|
|
1064
1100
|
} else {
|
|
1065
1101
|
yield {
|
|
1066
1102
|
type: "error",
|
|
1067
1103
|
message: e instanceof LoopError ? e.message : (e instanceof Error ? e.message : String(e)),
|
|
1068
1104
|
...(e instanceof LoopError ? { status: e.status } : {}),
|
|
1069
|
-
...(
|
|
1105
|
+
...(usage ? { usage } : {}),
|
|
1070
1106
|
};
|
|
1071
1107
|
}
|
|
1072
1108
|
return;
|
|
@@ -1079,13 +1115,15 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
1079
1115
|
}
|
|
1080
1116
|
|
|
1081
1117
|
const sse = bridgeToResponsesSSE(
|
|
1082
|
-
produce(), parsed._responseModelId ?? parsed.modelId, toolNsMap, freeform, toolSearch, () => {
|
|
1118
|
+
guardEmptyToolCatalogStream(produce(), deps.clientToolCatalogRestriction, translatorBudget), parsed._responseModelId ?? parsed.modelId, toolNsMap, freeform, toolSearch, () => {
|
|
1083
1119
|
internalAbort.abort("client closed responses stream");
|
|
1084
1120
|
}, 2_000,
|
|
1085
1121
|
{
|
|
1086
1122
|
translatorBudget,
|
|
1087
1123
|
replayCacheScope: parsed._reasoningReplayScope,
|
|
1124
|
+
...(deps.clientToolCatalogRestriction ? { ...deps.clientToolCatalogRestriction, enforceDeclaredToolNames: false } : {}),
|
|
1088
1125
|
...(deps.forceEmptyResponseId ? { responseId: "" } : {}),
|
|
1126
|
+
...(deps.terminalRefusalAsFailed ? { terminalRefusalAsFailed: true } : {}),
|
|
1089
1127
|
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
1090
1128
|
hideRawReasoning: parsed.options.hideRawReasoning,
|
|
1091
1129
|
stallTimeoutSec: deps.stallTimeoutSec,
|
|
@@ -1093,7 +1131,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
1093
1131
|
...(deps.onUsage ? {
|
|
1094
1132
|
// Terminal done/incomplete already includes hiddenUsage (merged above). Do not
|
|
1095
1133
|
// add it again here or request logs double-count multi-iteration image turns.
|
|
1096
|
-
onUsage: (usage: OcxUsage | undefined) => deps.onUsage?.(usage),
|
|
1134
|
+
onUsage: (usage: OcxUsage | undefined) => deps.onUsage?.(rotatedUsage ? ledgerUsage : usage),
|
|
1097
1135
|
} : {}),
|
|
1098
1136
|
...(deps.onCompletedResponse ? { onCompletedResponse: deps.onCompletedResponse } : {}),
|
|
1099
1137
|
},
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
/** Refresh only previously connected clients; a refused file never blocks its peers. */
|
|
13
13
|
export async function refreshOwnedCatalogIntegrations(
|
|
14
14
|
input: Omit<OwnedIntegrationRefreshInput, "clientId">,
|
|
15
|
-
clientIds: readonly IntegrationClientId[] = ["pi", "aside", "raycast", "omo", "commandcode", "droid", "opencode", "kilo", "qoder"],
|
|
15
|
+
clientIds: readonly IntegrationClientId[] = ["pi", "aside", "raycast", "omo", "commandcode", "droid", "opencode", "kilo", "qoder", "gentle-shell"],
|
|
16
16
|
options: { refreshOnly?: boolean; admit?: () => boolean } = {},
|
|
17
17
|
): Promise<OwnedIntegrationRefreshOutcome[]> {
|
|
18
18
|
// Client files are shared with the live proxy a sibling instance runs beside; their entries
|
|
@@ -134,6 +134,7 @@ const CLIENT_MANAGED_PATHS = {
|
|
|
134
134
|
qoder: [["providers", OPENCODE_PROVIDER_ID], ["modelConfigs", "customModels", DYNAMIC_SEGMENT]],
|
|
135
135
|
opencode: [["provider", OPENCODE_PROVIDER_ID], ["providers", OPENCODE_PROVIDER_ID]],
|
|
136
136
|
pi: [["providers", OPENCODE_PROVIDER_ID]],
|
|
137
|
+
"gentle-shell": [["providers", OPENCODE_PROVIDER_ID]],
|
|
137
138
|
omp: [["providers", OPENCODE_PROVIDER_ID]],
|
|
138
139
|
hermes: [["providers", OPENCODE_PROVIDER_ID]],
|
|
139
140
|
openclaw: [["models", "providers", OPENCODE_PROVIDER_ID]],
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* Design of record: devlog/_fin/260802_client_toggle_api/021 §1.
|
|
10
10
|
*/
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
|
+
import { gentleShellPaths } from "../clients/gentle-shell-path";
|
|
12
13
|
import { assertDroidSettingsUnambiguous } from "./droid-settings";
|
|
13
14
|
import { readPath } from "./merge";
|
|
14
15
|
import type { OwnershipRecord } from "./ownership";
|
|
@@ -505,6 +506,13 @@ export const INTEGRATION_CLIENTS: Record<IntegrationClientId, IntegrationClientS
|
|
|
505
506
|
return { configPath: droidConfigPath(env, home), detectDir };
|
|
506
507
|
},
|
|
507
508
|
},
|
|
509
|
+
"gentle-shell": {
|
|
510
|
+
id: "gentle-shell",
|
|
511
|
+
configPath: (env, home, io) => gentleShellPaths(env, home, io).configPath,
|
|
512
|
+
detectDir: (env, home, io) => gentleShellPaths(env, home, io).detectDir,
|
|
513
|
+
resolvePaths: (env, home, _context, io) => gentleShellPaths(env, home, io),
|
|
514
|
+
unresolvedPathHint: (_env, home = homedir()) => join(home, ".gentle-shell", "agent", "models.json"),
|
|
515
|
+
},
|
|
508
516
|
};
|
|
509
517
|
|
|
510
518
|
export const INTEGRATION_CLIENT_IDS: readonly IntegrationClientId[] =
|
|
@@ -36,6 +36,10 @@ export interface CodexAppServerStateResponse {
|
|
|
36
36
|
* The desktop half of a restart. Scalar-only for the same reason as the rest of this
|
|
37
37
|
* contract: pid lists and a closed-vocabulary reason, never a command line or an OS
|
|
38
38
|
* error message, both of which routinely embed a path or a username.
|
|
39
|
+
*
|
|
40
|
+
* `reason: "picker_current"` means the shell was deliberately left running because it
|
|
41
|
+
* launched after the current catalog was written, so only the app-servers restarted and
|
|
42
|
+
* the operator's unsaved client state was kept (#4761).
|
|
39
43
|
*/
|
|
40
44
|
export interface CodexDesktopRestartSummary {
|
|
41
45
|
attempted: boolean;
|
|
@@ -43,8 +47,17 @@ export interface CodexDesktopRestartSummary {
|
|
|
43
47
|
surviving: number[];
|
|
44
48
|
relaunch: "started" | "skipped";
|
|
45
49
|
reason?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Why the desktop was or was not restarted: `stale` (the shell predates the bound
|
|
52
|
+
* catalog, or this request rebound it) and `unknown` (the evidence was unreadable) both
|
|
53
|
+
* take the full restart. Absent on a proxy older than #4761.
|
|
54
|
+
*/
|
|
55
|
+
pickerState?: CodexDesktopPickerState;
|
|
46
56
|
}
|
|
47
57
|
|
|
58
|
+
export type CodexDesktopPickerState = "current" | "stale" | "unknown" | "not_running";
|
|
59
|
+
const DESKTOP_PICKER_STATES: readonly string[] = ["current", "stale", "unknown", "not_running"];
|
|
60
|
+
|
|
48
61
|
/** POST response. All four arrays are pid lists — never command lines. */
|
|
49
62
|
export interface CodexRestartResponse {
|
|
50
63
|
success: boolean;
|
|
@@ -130,6 +143,8 @@ export function isCodexRestartResponse(value: unknown): value is CodexRestartRes
|
|
|
130
143
|
if (!isPidList(d.stopped) || !isPidList(d.surviving)) return false;
|
|
131
144
|
if (d.relaunch !== "started" && d.relaunch !== "skipped") return false;
|
|
132
145
|
if (d.reason !== undefined && typeof d.reason !== "string") return false;
|
|
146
|
+
if (d.pickerState !== undefined
|
|
147
|
+
&& (typeof d.pickerState !== "string" || !DESKTOP_PICKER_STATES.includes(d.pickerState))) return false;
|
|
133
148
|
// A started relaunch cannot have left anything behind: the ladder refuses to
|
|
134
149
|
// relaunch beside a survivor precisely so a second shell never appears.
|
|
135
150
|
if (d.relaunch === "started" && (d.surviving as number[]).length > 0) return false;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { connect as tcpConnect,
|
|
2
|
-
import {
|
|
1
|
+
import { connect as tcpConnect, type Socket } from "node:net";
|
|
2
|
+
import { connect as tlsConnect, type TLSSocket } from "node:tls";
|
|
3
3
|
import { desktopProxyFor } from "./desktop-proxy-route";
|
|
4
|
-
import {
|
|
4
|
+
import { dialTunnel, type OpaqueProxyRoute } from "./opaque-proxy-tunnel";
|
|
5
5
|
|
|
6
|
-
//
|
|
7
|
-
//
|
|
6
|
+
// The shared opaque dialer owns proxy framing; this caller owns fixed-target
|
|
7
|
+
// TLS and bounds the entire connection, including the target TLS handshake.
|
|
8
8
|
export interface DesktopTunnelOptions {
|
|
9
9
|
proxy?: string | false;
|
|
10
10
|
signal?: AbortSignal;
|
|
@@ -15,37 +15,6 @@ export interface DesktopTunnelOptions {
|
|
|
15
15
|
proxyCa?: string;
|
|
16
16
|
}
|
|
17
17
|
const fail = () => new Error("desktop_upstream_connection_failed");
|
|
18
|
-
class HandshakeReader {
|
|
19
|
-
private buffer: Buffer<ArrayBufferLike> = Buffer.alloc(0);
|
|
20
|
-
private failure: Error | null = null;
|
|
21
|
-
private waiting: { ready: () => boolean; resolve: () => void; reject: (error: Error) => void } | null = null;
|
|
22
|
-
private onData = (chunk: Buffer) => {
|
|
23
|
-
if (this.buffer.length + chunk.length > 65536) { this.onFailure(); return; }
|
|
24
|
-
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
25
|
-
if (this.waiting?.ready()) { const current = this.waiting; this.waiting = null; current.resolve(); }
|
|
26
|
-
};
|
|
27
|
-
private onFailure = () => { this.failure = fail(); const current = this.waiting; this.waiting = null; current?.reject(this.failure); };
|
|
28
|
-
constructor(private socket: Socket, private signal: AbortSignal) {
|
|
29
|
-
socket.on("data", this.onData); socket.once("error", this.onFailure); socket.once("end", this.onFailure); socket.once("close", this.onFailure);
|
|
30
|
-
signal.addEventListener("abort", this.onFailure, { once: true }); if (signal.aborted) this.onFailure(); socket.resume();
|
|
31
|
-
}
|
|
32
|
-
write(bytes: Uint8Array | string): void { if (this.failure) throw this.failure; this.socket.write(bytes); }
|
|
33
|
-
private async until(ready: () => boolean): Promise<void> {
|
|
34
|
-
if (this.failure) throw this.failure;
|
|
35
|
-
if (ready()) return;
|
|
36
|
-
if (this.waiting) throw fail();
|
|
37
|
-
await new Promise<void>((resolve, reject) => { this.waiting = { ready, resolve, reject }; });
|
|
38
|
-
}
|
|
39
|
-
private take(length: number): Buffer { const out = this.buffer.subarray(0, length); this.buffer = this.buffer.subarray(length); return out; }
|
|
40
|
-
async readExact(length: number): Promise<Buffer> { await this.until(() => this.buffer.length >= length); return this.take(length); }
|
|
41
|
-
async readHead(): Promise<string> { await this.until(() => this.buffer.indexOf("\r\n\r\n") >= 0); return this.take(this.buffer.indexOf("\r\n\r\n") + 4).toString("latin1"); }
|
|
42
|
-
dispose(): void {
|
|
43
|
-
this.socket.pause(); this.socket.removeListener("data", this.onData); this.socket.removeListener("error", this.onFailure);
|
|
44
|
-
this.socket.removeListener("end", this.onFailure); this.socket.removeListener("close", this.onFailure);
|
|
45
|
-
this.signal.removeEventListener("abort", this.onFailure);
|
|
46
|
-
if (this.buffer.length) this.socket.unshift(this.buffer); this.buffer = Buffer.alloc(0); this.onFailure();
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
18
|
function waitConnected(socket: Socket, event: "connect" | "secureConnect", signal: AbortSignal): Promise<void> {
|
|
50
19
|
return new Promise((resolve, reject) => {
|
|
51
20
|
const dispose = () => { socket.off(event, done); socket.off("error", failure); socket.off("close", failure); signal.removeEventListener("abort", failure); };
|
|
@@ -76,25 +45,21 @@ export async function dialDesktopUpstream(options: DesktopTunnelOptions = {}): P
|
|
|
76
45
|
const url = new URL(proxy), protocol = url.protocol;
|
|
77
46
|
if (!["http:", "https:", "socks5:", "socks5h:"].includes(protocol) || !url.hostname || url.search || url.hash || url.pathname !== "" && url.pathname !== "/") throw fail();
|
|
78
47
|
const host = url.hostname.replace(/^\[|\]$/g, ""), port = Number(url.port) || (protocol === "https:" ? 443 : protocol.startsWith("socks") ? 1080 : 80);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const authority = `${target.host}:${target.port}`;
|
|
90
|
-
const lines = [`CONNECT ${authority} HTTP/1.1`, `Host: ${authority}`];
|
|
91
|
-
if (url.username || url.password) lines.push(`Proxy-Authorization: Basic ${Buffer.from(`${decodeURIComponent(url.username)}:${decodeURIComponent(url.password)}`).toString("base64")}`);
|
|
92
|
-
reader.write(lines.join("\r\n") + "\r\n\r\n");
|
|
93
|
-
if (!/^HTTP\/1\.[01] 2\d\d(?: |\r)/.test(await reader.readHead())) throw fail();
|
|
94
|
-
}
|
|
95
|
-
} finally { reader.dispose(); }
|
|
48
|
+
const credentials = url.username || url.password
|
|
49
|
+
? { username: decodeURIComponent(url.username), password: decodeURIComponent(url.password) }
|
|
50
|
+
: {};
|
|
51
|
+
const endpoint = { host, port, ...credentials };
|
|
52
|
+
const route: OpaqueProxyRoute = protocol === "https:"
|
|
53
|
+
? { ...endpoint, kind: "https", proxyCa: options.proxyCa }
|
|
54
|
+
: { ...endpoint, kind: protocol.slice(0, -1) as "http" | "socks5" | "socks5h" };
|
|
55
|
+
const authority = `${target.host.includes(":") ? `[${target.host}]` : target.host}:${target.port}`;
|
|
56
|
+
// The route resolver remains authoritative; a failed proxy never becomes DIRECT.
|
|
57
|
+
raw = own(await dialTunnel(authority, route, { signal, timeoutMs: timeout, maxHeaderBytes: 65536 }));
|
|
96
58
|
}
|
|
97
59
|
if (signal.aborted || raw.destroyed) throw fail();
|
|
60
|
+
// This consumer historically closes its writable side after peer EOF. The
|
|
61
|
+
// shared opaque transport preserves half-closes for callers that need them.
|
|
62
|
+
raw.allowHalfOpen = false;
|
|
98
63
|
const secured = own(tlsConnect({ socket: raw, servername: "chatgpt.com", ca: options.ca, rejectUnauthorized: true, ALPNProtocols: ["http/1.1"] }));
|
|
99
64
|
// The CONNECT reader paused its stream before handing buffered bytes back. Bun's
|
|
100
65
|
// TLS-over-TLS path needs the outer TLS stream explicitly resumed for the new handshake.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { normalizeDeclaredToolName, type AdapterEvent } from "../types";
|
|
2
|
+
import { UNDECLARED_TOOL_CALL_ERROR_CODE } from "./errors";
|
|
3
|
+
import { releaseTranslatedEvent, type TranslatorBudget } from "./translator-budget";
|
|
4
|
+
import { releaseCompactionCiphertextLease } from "../responses/compaction";
|
|
5
|
+
|
|
6
|
+
/** Snapshot of current-turn declarations when an explicit empty catalog activates enforcement. */
|
|
7
|
+
export interface ClientToolCatalogRestriction {
|
|
8
|
+
declaredToolNames: ReadonlySet<string>;
|
|
9
|
+
bareCustomToolNames: ReadonlySet<string>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function emptyToolCatalogRefusal(
|
|
13
|
+
event: AdapterEvent,
|
|
14
|
+
catalog: ClientToolCatalogRestriction | undefined,
|
|
15
|
+
): Extract<AdapterEvent, { type: "error" }> | undefined {
|
|
16
|
+
if (!catalog) return;
|
|
17
|
+
if (event.type === "tool_call_start") {
|
|
18
|
+
const name = normalizeDeclaredToolName(event.name, catalog.declaredToolNames, undefined, catalog.bareCustomToolNames);
|
|
19
|
+
if (catalog.declaredToolNames.has(name)) return;
|
|
20
|
+
} else if (catalog.declaredToolNames.size !== 0
|
|
21
|
+
|| (event.type !== "tool_call_delta" && event.type !== "tool_call_end")) return;
|
|
22
|
+
return {
|
|
23
|
+
type: "error", status: 502, errorType: "upstream_error", code: UNDECLARED_TOOL_CALL_ERROR_CODE,
|
|
24
|
+
message: "routed provider emitted a client tool call outside the current request's explicit tool catalog",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Runs after hosted tool loops, before client delivery and continuation-state folding. */
|
|
29
|
+
export async function* guardEmptyToolCatalogStream(
|
|
30
|
+
events: AsyncIterable<AdapterEvent>,
|
|
31
|
+
catalog: ClientToolCatalogRestriction | undefined,
|
|
32
|
+
budget: TranslatorBudget,
|
|
33
|
+
): AsyncGenerator<AdapterEvent> {
|
|
34
|
+
for await (const event of events) {
|
|
35
|
+
const refusal = emptyToolCatalogRefusal(event, catalog);
|
|
36
|
+
if (refusal) {
|
|
37
|
+
releaseTranslatedEvent(event, budget);
|
|
38
|
+
releaseCompactionCiphertextLease(event, budget);
|
|
39
|
+
}
|
|
40
|
+
yield refusal ?? event;
|
|
41
|
+
if (refusal) return;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function guardEmptyToolCatalogEvents(
|
|
46
|
+
events: AdapterEvent[],
|
|
47
|
+
catalog: ClientToolCatalogRestriction | undefined,
|
|
48
|
+
budget: TranslatorBudget,
|
|
49
|
+
): AdapterEvent[] {
|
|
50
|
+
if (!catalog) return events;
|
|
51
|
+
const index = events.findIndex(event => emptyToolCatalogRefusal(event, catalog) !== undefined);
|
|
52
|
+
if (index < 0) return events;
|
|
53
|
+
const refusal = emptyToolCatalogRefusal(events[index]!, catalog)!;
|
|
54
|
+
for (const event of events.slice(index)) {
|
|
55
|
+
if ((event.type === "done" || event.type === "incomplete" || event.type === "error") && event.usage) {
|
|
56
|
+
refusal.usage = event.usage;
|
|
57
|
+
}
|
|
58
|
+
releaseTranslatedEvent(event, budget);
|
|
59
|
+
releaseCompactionCiphertextLease(event, budget);
|
|
60
|
+
}
|
|
61
|
+
return [...events.slice(0, index), refusal];
|
|
62
|
+
}
|
package/src/lib/errors.ts
CHANGED
|
@@ -650,10 +650,22 @@ export function adapterFailureFromMessage(message: string): { httpStatus: number
|
|
|
650
650
|
return { httpStatus, error };
|
|
651
651
|
}
|
|
652
652
|
|
|
653
|
+
/** A declared client refusal: retrying the identical request cannot change the verdict. */
|
|
654
|
+
const CLIENT_ERROR_STATUS: Record<string, 400 | 401 | 403 | 404> = {
|
|
655
|
+
invalid_request_error: 400,
|
|
656
|
+
authentication_error: 401,
|
|
657
|
+
permission_error: 403,
|
|
658
|
+
not_found_error: 404,
|
|
659
|
+
};
|
|
660
|
+
|
|
653
661
|
/** Narrow structured classes carried through a synthesized bare-error terminal. */
|
|
654
662
|
export function recognizedUpstreamError(error: { type?: unknown; code?: unknown }):
|
|
655
|
-
{ type: string; code: string; httpStatus: 429 | 503 } | undefined {
|
|
663
|
+
{ type: string; code: string; httpStatus: 400 | 401 | 403 | 404 | 429 | 503 } | undefined {
|
|
656
664
|
const code = typeof error.code === "string" ? error.code : undefined;
|
|
665
|
+
const clientClass = code ?? (typeof error.type === "string" ? error.type : undefined);
|
|
666
|
+
if (clientClass !== undefined && Object.hasOwn(CLIENT_ERROR_STATUS, clientClass)) {
|
|
667
|
+
return { type: clientClass, code: clientClass, httpStatus: CLIENT_ERROR_STATUS[clientClass] };
|
|
668
|
+
}
|
|
657
669
|
// An explicit unknown code must not gain a recognized class from its type or copy.
|
|
658
670
|
if (code !== undefined && !["rate_limit_exceeded", "rate_limit_error", "server_is_overloaded", "overloaded_error"].includes(code)) return undefined;
|
|
659
671
|
if (code === "rate_limit_exceeded" || code === "rate_limit_error"
|
|
@@ -680,8 +692,13 @@ export function httpStatusFromTerminalError(error: {
|
|
|
680
692
|
if (isCyberPolicyCode(error.code) || (error.message ? isCyberPolicyMessage(error.message) : false)) {
|
|
681
693
|
return 400;
|
|
682
694
|
}
|
|
695
|
+
const message = error.message ?? "";
|
|
683
696
|
const recognized = recognizedUpstreamError(error);
|
|
684
|
-
|
|
697
|
+
// Client-closed messages often arrive as invalid_request_error after classifyError, so a
|
|
698
|
+
// recognized 400 yields to the client-closed check below; every other class keeps its order.
|
|
699
|
+
if (recognized && !(recognized.httpStatus === 400 && message && isClientClosedMessage(message))) {
|
|
700
|
+
return recognized.httpStatus;
|
|
701
|
+
}
|
|
685
702
|
if (error.type === "rate_limit_error" || error.code === "rate_limit_exceeded") return 429;
|
|
686
703
|
if (error.type === "authentication_error" || error.code === "invalid_api_key") return 401;
|
|
687
704
|
if (
|
|
@@ -693,7 +710,6 @@ export function httpStatusFromTerminalError(error: {
|
|
|
693
710
|
if (error.type === "server_error" && error.code === "server_is_overloaded") return 503;
|
|
694
711
|
// Client-closed messages often arrive as invalid_request_error after classifyError; check message
|
|
695
712
|
// before treating every invalid_request_error as HTTP 400.
|
|
696
|
-
const message = error.message ?? "";
|
|
697
713
|
if (message && isClientClosedMessage(message)) return 499;
|
|
698
714
|
if (error.type === "invalid_request_error") return 400;
|
|
699
715
|
if (error.type === "proxy_error") return 500;
|
package/src/lib/gcp-adc.ts
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
* Application Default Credentials (ADC) resolution for Vertex AI.
|
|
3
3
|
*
|
|
4
4
|
* Direct WebCrypto + REST implementation (no `google-auth-library`). Sources, in priority order:
|
|
5
|
+
* 0. A `gcp-sa:<account>` reference (the provider's active key resolving to one) → the Service
|
|
6
|
+
* Account / authorized_user JSON stored in the OS credential store under that account
|
|
7
|
+
* (`keychain:<account>` secret, written by the key-store save path when the user pastes a
|
|
8
|
+
* credential JSON directly into the API-key slot). See resolveGcpCredentialMarker.
|
|
5
9
|
* 1. `GOOGLE_APPLICATION_CREDENTIALS` env → file with `type: "service_account"` (RS256 JWT
|
|
6
10
|
* exchange) or `type: "authorized_user"` (refresh-token exchange).
|
|
7
11
|
* 2. gcloud user ADC (`CLOUDSDK_CONFIG`, else `%APPDATA%\gcloud` on Windows, else
|
|
@@ -18,6 +22,8 @@ import { Buffer } from "node:buffer";
|
|
|
18
22
|
import * as os from "node:os";
|
|
19
23
|
import * as path from "node:path";
|
|
20
24
|
import { readFileSync, existsSync, statSync } from "node:fs";
|
|
25
|
+
import { GCP_CREDENTIAL_MARKER_PREFIX, providerKeychainEntry } from "../providers/api-key-resolve";
|
|
26
|
+
export { isGcpCredentialMaterial } from "./gcp-credential-input";
|
|
21
27
|
import {
|
|
22
28
|
captureConfigGeneration,
|
|
23
29
|
sweepExpiredOnWrite,
|
|
@@ -36,6 +42,50 @@ const TOKEN_TIMEOUT_MS = 15_000;
|
|
|
36
42
|
const TOKEN_ATTEMPTS = 3;
|
|
37
43
|
const TOKEN_RETRY_BASE_MS = 300;
|
|
38
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Reference prefix stored in `provider.apiKey` (and `apiKeyPool` entries) when the user pastes a
|
|
47
|
+
* GCP credential JSON directly into an API-key slot. The secret JSON lives in the OS credential
|
|
48
|
+
* store under the same service the `keychain:` references use; only the marker is persisted to
|
|
49
|
+
* `config.json`. The leaf reference prefix also identifies already-stored material for migration.
|
|
50
|
+
*/
|
|
51
|
+
export { GCP_CREDENTIAL_MARKER_PREFIX } from "../providers/api-key-resolve";
|
|
52
|
+
|
|
53
|
+
/** Extract a marker account; malformed explicit markers never opt into host ADC. */
|
|
54
|
+
export function gcpCredentialMarkerAccount(value: string | undefined): string | undefined {
|
|
55
|
+
if (typeof value !== "string" || !value.startsWith(GCP_CREDENTIAL_MARKER_PREFIX)) return undefined;
|
|
56
|
+
const account = value.slice(GCP_CREDENTIAL_MARKER_PREFIX.length);
|
|
57
|
+
if (!account.trim()) throw new Error("GCP credential marker requires a non-empty account");
|
|
58
|
+
return account;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Whether a pasted API-key value is in fact a GCP credential JSON. Used by the save path to
|
|
63
|
+
* divert JSON material out of the literal-key flow before it reaches `config.json`. Deliberately
|
|
64
|
+
* strict: only a `type` field with a known ADC credential type counts, so a user's literal API
|
|
65
|
+
* key that happens to start with `{` is never eaten.
|
|
66
|
+
*/
|
|
67
|
+
export function parseGcpCredentialJson(value: string): AdcFileCredentials | undefined {
|
|
68
|
+
const trimmed = (value ?? "").trim();
|
|
69
|
+
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return undefined;
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(trimmed) as Record<string, unknown>;
|
|
72
|
+
if (parsed?.type === "service_account") {
|
|
73
|
+
if (typeof parsed.client_email !== "string" || !parsed.client_email.trim()) return undefined;
|
|
74
|
+
if (typeof parsed.private_key !== "string" || !parsed.private_key.trim()) return undefined;
|
|
75
|
+
return parsed as unknown as AdcFileCredentials;
|
|
76
|
+
}
|
|
77
|
+
if (parsed?.type === "authorized_user") {
|
|
78
|
+
if (typeof parsed.client_id !== "string" || !parsed.client_id.trim()) return undefined;
|
|
79
|
+
if (typeof parsed.client_secret !== "string" || !parsed.client_secret.trim()) return undefined;
|
|
80
|
+
if (typeof parsed.refresh_token !== "string" || !parsed.refresh_token.trim()) return undefined;
|
|
81
|
+
return parsed as unknown as AdcFileCredentials;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
} catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
39
89
|
interface CachedToken {
|
|
40
90
|
token: string;
|
|
41
91
|
expiresAtMs: number;
|
|
@@ -81,7 +131,10 @@ export function sweepExpiredGcpAdcTokens(now = Date.now()): number {
|
|
|
81
131
|
|
|
82
132
|
export function reconcileGcpAdcTokens(context: GenerationContext): number {
|
|
83
133
|
if (context.generation <= lastReconciledGeneration) return 0;
|
|
84
|
-
|
|
134
|
+
// The periodic sweep registration has no request context, so no marker is active here: markers
|
|
135
|
+
// travel per-request via getVertexAccessToken(options.markerAccount), and a marker-token cache
|
|
136
|
+
// entry whose account no longer resolves is simply pruned by this sweep when it expires.
|
|
137
|
+
const currentSource = currentAdcSourceKey(undefined);
|
|
85
138
|
let removed = 0;
|
|
86
139
|
for (const source of tokenCache.keys()) {
|
|
87
140
|
if (source === currentSource) continue;
|
|
@@ -131,7 +184,29 @@ function readJsonFile<T>(filePath: string): T | undefined {
|
|
|
131
184
|
return JSON.parse(readFileSync(filePath, "utf8")) as T;
|
|
132
185
|
}
|
|
133
186
|
|
|
134
|
-
|
|
187
|
+
/**
|
|
188
|
+
* Read the credential JSON for a `gcp-sa:<account>` marker from the OS credential store. Uses the
|
|
189
|
+
* same service name and the same injectable entry factory as the `keychain:` API-key references
|
|
190
|
+
* (api-key-resolve.providerKeychainEntry), so one probe surface and one test seam cover both.
|
|
191
|
+
* Mirrors `readKeychain`'s fail-closed policy: an unreadable entry yields no credential (the
|
|
192
|
+
* caller surfaces the failure instead of silently falling through to the next ADC source).
|
|
193
|
+
*/
|
|
194
|
+
function readMarkerCredential(account: string): AdcFileCredentials | undefined {
|
|
195
|
+
try {
|
|
196
|
+
const raw = providerKeychainEntry(account).getPassword();
|
|
197
|
+
if (typeof raw !== "string" || !raw.trim()) return undefined;
|
|
198
|
+
return parseGcpCredentialJson(raw);
|
|
199
|
+
} catch {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function loadAdcCredentials(markerAccount: string | undefined): { source: string; creds: AdcFileCredentials } | undefined {
|
|
205
|
+
if (markerAccount) {
|
|
206
|
+
const creds = readMarkerCredential(markerAccount);
|
|
207
|
+
if (!creds) throw new Error("GCP credential marker is set but its credential could not be read from the OS credential store");
|
|
208
|
+
return { source: `marker:${markerAccount}`, creds };
|
|
209
|
+
}
|
|
135
210
|
const gacPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
|
136
211
|
if (gacPath) {
|
|
137
212
|
const creds = readJsonFile<AdcFileCredentials>(gacPath);
|
|
@@ -148,9 +223,10 @@ function loadAdcCredentials(): { source: string; creds: AdcFileCredentials } | u
|
|
|
148
223
|
* The cache key for the source the NEXT resolve would use, computed cheaply (no network). Lets the
|
|
149
224
|
* cache return a token only when it still matches the active credential source, so an in-process
|
|
150
225
|
* change to GOOGLE_APPLICATION_CREDENTIALS (or the user ADC file) does not keep serving a stale
|
|
151
|
-
* token from a different source. Falls back to "metadata" when no file/env ADC is present.
|
|
226
|
+
* token from a different source. Falls back to "metadata" when no marker/file/env ADC is present.
|
|
152
227
|
*/
|
|
153
|
-
function currentAdcSourceKey(): string {
|
|
228
|
+
function currentAdcSourceKey(markerAccount: string | undefined): string {
|
|
229
|
+
if (markerAccount) return `marker:${markerAccount}`;
|
|
154
230
|
const gacPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
|
155
231
|
if (gacPath) return fileSourceTag("gac", gacPath);
|
|
156
232
|
const userPath = userAdcPath();
|
|
@@ -279,8 +355,8 @@ async function fetchMetadataToken(signal: AbortSignal | undefined, fetchImpl: Fe
|
|
|
279
355
|
}
|
|
280
356
|
}
|
|
281
357
|
|
|
282
|
-
async function resolveAccessTokenUncached(signal: AbortSignal | undefined, fetchImpl: FetchImpl): Promise<{ source: string; token: TokenResponse }> {
|
|
283
|
-
const adc = loadAdcCredentials();
|
|
358
|
+
async function resolveAccessTokenUncached(markerAccount: string | undefined, signal: AbortSignal | undefined, fetchImpl: FetchImpl): Promise<{ source: string; token: TokenResponse }> {
|
|
359
|
+
const adc = loadAdcCredentials(markerAccount);
|
|
284
360
|
if (adc) {
|
|
285
361
|
const token = adc.creds.type === "service_account"
|
|
286
362
|
? await exchangeJwtForToken(adc.creds, signal, fetchImpl)
|
|
@@ -295,14 +371,25 @@ async function resolveAccessTokenUncached(signal: AbortSignal | undefined, fetch
|
|
|
295
371
|
}
|
|
296
372
|
|
|
297
373
|
/** Returns a Bearer access token for the `Authorization` header on Vertex AI calls (cached + refreshed). */
|
|
298
|
-
export async function getVertexAccessToken(options?: { signal?: AbortSignal; fetch?: FetchImpl }): Promise<string> {
|
|
374
|
+
export async function getVertexAccessToken(options?: { signal?: AbortSignal; fetch?: FetchImpl; markerAccount?: string }): Promise<string> {
|
|
375
|
+
// The marker travels as an argument, never as process-global state: a module-level "active
|
|
376
|
+
// marker" written by the router would race across concurrent requests for different providers
|
|
377
|
+
// and could resolve one provider's Vertex call with another provider's service account. The
|
|
378
|
+
// adapter passes `gcpCredentialMarkerAccount(provider.apiKey)` per request instead.
|
|
379
|
+
const markerAccount = options?.markerAccount;
|
|
299
380
|
const fetchImpl = options?.fetch ?? globalThis.fetch.bind(globalThis);
|
|
300
381
|
const skew = getRefreshSkewMs();
|
|
301
382
|
const now = Date.now();
|
|
302
383
|
|
|
303
384
|
// Only serve a cached token that matches the source the next resolve would actually use; prune
|
|
304
385
|
// expired or now-stale (different-source) entries so a credential-source change is honored.
|
|
305
|
-
const expectedSource = currentAdcSourceKey();
|
|
386
|
+
const expectedSource = currentAdcSourceKey(markerAccount);
|
|
387
|
+
// A cached bearer token is usable only while the explicitly selected store entry remains
|
|
388
|
+
// readable. Removal or a locked store must fail closed before the cache can return it.
|
|
389
|
+
if (markerAccount && !readMarkerCredential(markerAccount)) {
|
|
390
|
+
tokenCache.delete(expectedSource);
|
|
391
|
+
throw new Error("GCP credential marker is set but its credential could not be read from the OS credential store");
|
|
392
|
+
}
|
|
306
393
|
const writerGeneration = captureConfigGeneration();
|
|
307
394
|
for (const [source, cached] of tokenCache) {
|
|
308
395
|
if (source === expectedSource && cached.expiresAtMs - skew > now) return cached.token;
|
|
@@ -317,7 +404,7 @@ export async function getVertexAccessToken(options?: { signal?: AbortSignal; fet
|
|
|
317
404
|
|
|
318
405
|
const promise = (async () => {
|
|
319
406
|
try {
|
|
320
|
-
const { source, token } = await resolveAccessTokenUncached(options?.signal, fetchImpl);
|
|
407
|
+
const { source, token } = await resolveAccessTokenUncached(markerAccount, options?.signal, fetchImpl);
|
|
321
408
|
const expiresAtMs = Date.now() + Math.max(0, token.expires_in * 1000);
|
|
322
409
|
if (writerGeneration >= lastReconciledGeneration || liveSources.has(source)) {
|
|
323
410
|
tokenCache.set(source, { token: token.access_token, expiresAtMs });
|