mixdog 0.7.1
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/.claude-plugin/marketplace.json +31 -0
- package/.claude-plugin/plugin.json +20 -0
- package/.gitattributes +34 -0
- package/.mcp.json +14 -0
- package/ARCHITECTURE.md +77 -0
- package/CHANGELOG.md +7 -0
- package/CONTRIBUTING.md +45 -0
- package/DATA-FLOW.md +79 -0
- package/LICENSE +21 -0
- package/README.md +389 -0
- package/SECURITY.md +138 -0
- package/UNINSTALL.md +112 -0
- package/agents/maintenance.md +5 -0
- package/agents/memory-classification.md +30 -0
- package/agents/scheduler-task.md +18 -0
- package/agents/webhook-handler.md +27 -0
- package/agents/worker.md +24 -0
- package/bin/bridge +133 -0
- package/bin/statusline-launcher.mjs +78 -0
- package/bin/statusline-lib.mjs +550 -0
- package/bin/statusline.mjs +607 -0
- package/bun.lock +802 -0
- package/commands/config.md +16 -0
- package/commands/doctor.md +13 -0
- package/commands/setup.md +17 -0
- package/defaults/cycle3-review-prompt.md +90 -0
- package/defaults/hidden-roles.json +65 -0
- package/defaults/memory-chunk-prompt.md +63 -0
- package/defaults/memory-promote-prompt.md +135 -0
- package/defaults/mixdog-config.template.json +27 -0
- package/defaults/user-workflow.json +8 -0
- package/defaults/user-workflow.md +12 -0
- package/hooks/hooks.json +73 -0
- package/hooks/lib/active-instance.cjs +77 -0
- package/hooks/lib/permission-evaluator.cjs +411 -0
- package/hooks/lib/permission-route.cjs +63 -0
- package/hooks/lib/permission-rules.cjs +170 -0
- package/hooks/lib/settings-loader.cjs +116 -0
- package/hooks/post-tool-use.cjs +84 -0
- package/hooks/pre-mcp-sandbox.cjs +158 -0
- package/hooks/pre-tool-subagent.cjs +253 -0
- package/hooks/session-start.cjs +1372 -0
- package/hooks/turn-timer.cjs +82 -0
- package/lib/claude-md-writer.cjs +386 -0
- package/lib/config-cjs.cjs +61 -0
- package/lib/hook-pipe-path.cjs +10 -0
- package/lib/keychain-cjs.cjs +263 -0
- package/lib/plugin-paths.cjs +61 -0
- package/lib/rules-builder.cjs +241 -0
- package/lib/text-utils.cjs +61 -0
- package/native/README.md +117 -0
- package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
- package/package.json +107 -0
- package/prompts/code-review.txt +16 -0
- package/prompts/security-audit.txt +17 -0
- package/rules/bridge/00-common.md +39 -0
- package/rules/bridge/20-skip-protocol.md +18 -0
- package/rules/bridge/30-explorer.md +33 -0
- package/rules/bridge/40-cycle1-agent.md +52 -0
- package/rules/bridge/41-cycle2-agent.md +62 -0
- package/rules/bridge/42-cycle3-agent.md +44 -0
- package/rules/lead/00-tool-lead.md +61 -0
- package/rules/lead/01-general.md +23 -0
- package/rules/lead/02-channels.md +49 -0
- package/rules/lead/03-team.md +27 -0
- package/rules/lead/04-workflow.md +20 -0
- package/rules/shared/00-language.md +14 -0
- package/rules/shared/01-tool.md +138 -0
- package/scripts/bootstrap.mjs +184 -0
- package/scripts/bridge-unify-smoke.mjs +308 -0
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/builtin-utils-smoke.mjs +392 -0
- package/scripts/check-json.mjs +45 -0
- package/scripts/check-syntax-changed.mjs +102 -0
- package/scripts/check-syntax.mjs +58 -0
- package/scripts/code-graph-batch.test.mjs +33 -0
- package/scripts/config-preserve-smoke.mjs +180 -0
- package/scripts/doctor.mjs +484 -0
- package/scripts/edit-normalize-fuzz.mjs +130 -0
- package/scripts/edit-normalize-smoke.mjs +401 -0
- package/scripts/edit-operation-smoke.mjs +369 -0
- package/scripts/edit2-smoke.mjs +63 -0
- package/scripts/fuzzy-e2e.mjs +28 -0
- package/scripts/fuzzy-smoke.mjs +26 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/guard-smoke.mjs +66 -0
- package/scripts/hidden-role-schema-smoke.mjs +162 -0
- package/scripts/hook-routing-smoke.mjs +29 -0
- package/scripts/inject-input.ps1 +204 -0
- package/scripts/io-complex-smoke.mjs +667 -0
- package/scripts/io-explore-bench.mjs +424 -0
- package/scripts/io-guardrails-smoke.mjs +205 -0
- package/scripts/io-mini-bench-baseline.json +11 -0
- package/scripts/io-mini-bench.mjs +216 -0
- package/scripts/io-route-harness.mjs +933 -0
- package/scripts/io-telemetry-report.mjs +691 -0
- package/scripts/mutation-bench.mjs +564 -0
- package/scripts/mutation-io-smoke.mjs +1081 -0
- package/scripts/native-patch-bridge-smoke.mjs +288 -0
- package/scripts/native-patch-smoke.mjs +304 -0
- package/scripts/patch-interior-context-smoke.mjs +49 -0
- package/scripts/patch-newline-utf8-smoke.mjs +157 -0
- package/scripts/perf-hook-smoke.mjs +71 -0
- package/scripts/permission-eval-smoke.mjs +426 -0
- package/scripts/prep-patch.mjs +53 -0
- package/scripts/prep-shim.mjs +96 -0
- package/scripts/provider-cache-smoke.mjs +687 -0
- package/scripts/report-runtime-health.mjs +132 -0
- package/scripts/run-mcp.mjs +1547 -0
- package/scripts/salvage-v4a-shatter.test.mjs +58 -0
- package/scripts/scoped-cache-io-smoke.mjs +103 -0
- package/scripts/shell-policy-round3-smoke.mjs +46 -0
- package/scripts/smoke-runtime-negative.ps1 +100 -0
- package/scripts/smoke-runtime-negative.sh +95 -0
- package/scripts/stall-policy-smoke.mjs +50 -0
- package/scripts/start-memory-worker.mjs +23 -0
- package/scripts/statusline-launcher-smoke.mjs +82 -0
- package/scripts/stress-atomic-write.mjs +1028 -0
- package/scripts/test-config-rmw-restore.mjs +122 -0
- package/scripts/test-fault-inject.mjs +164 -0
- package/scripts/test-large-file.mjs +174 -0
- package/scripts/tool-edge-smoke.mjs +209 -0
- package/scripts/uninstall.mjs +201 -0
- package/scripts/webhook-selfheal-smoke.mjs +29 -0
- package/scripts/write-overwrite-guard-smoke.mjs +56 -0
- package/server-main.mjs +3055 -0
- package/server.mjs +468 -0
- package/setup/config-merge.mjs +254 -0
- package/setup/install.mjs +120 -0
- package/setup/launch-core.mjs +507 -0
- package/setup/launch.mjs +101 -0
- package/setup/setup-server.mjs +3206 -0
- package/setup/setup.html +3693 -0
- package/skills/retro-skill-proposer/SKILL.md +92 -0
- package/skills/schedule-add/SKILL.md +77 -0
- package/skills/setup/SKILL.md +346 -0
- package/skills/webhook-add/SKILL.md +81 -0
- package/src/agent/bridge-stall-watchdog.mjs +337 -0
- package/src/agent/index.mjs +2138 -0
- package/src/agent/orchestrator/activity-bus.mjs +38 -0
- package/src/agent/orchestrator/ai-wrapped-dispatch.mjs +1010 -0
- package/src/agent/orchestrator/bridge-retry.mjs +220 -0
- package/src/agent/orchestrator/bridge-trace.mjs +583 -0
- package/src/agent/orchestrator/cache-mtime.mjs +58 -0
- package/src/agent/orchestrator/config.mjs +358 -0
- package/src/agent/orchestrator/context/collect.mjs +651 -0
- package/src/agent/orchestrator/dispatch-persist.mjs +549 -0
- package/src/agent/orchestrator/drain-registry.mjs +50 -0
- package/src/agent/orchestrator/explore-validator.mjs +8 -0
- package/src/agent/orchestrator/internal-roles.mjs +118 -0
- package/src/agent/orchestrator/internal-tools.mjs +88 -0
- package/src/agent/orchestrator/jobs.mjs +116 -0
- package/src/agent/orchestrator/mcp/client.mjs +364 -0
- package/src/agent/orchestrator/providers/anthropic-betas.mjs +21 -0
- package/src/agent/orchestrator/providers/anthropic-oauth.mjs +1745 -0
- package/src/agent/orchestrator/providers/anthropic.mjs +437 -0
- package/src/agent/orchestrator/providers/gemini.mjs +1175 -0
- package/src/agent/orchestrator/providers/grok-oauth.mjs +782 -0
- package/src/agent/orchestrator/providers/model-catalog.mjs +241 -0
- package/src/agent/orchestrator/providers/openai-compat.mjs +1467 -0
- package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +1890 -0
- package/src/agent/orchestrator/providers/openai-oauth.mjs +1307 -0
- package/src/agent/orchestrator/providers/openai-ws.mjs +104 -0
- package/src/agent/orchestrator/providers/registry.mjs +192 -0
- package/src/agent/orchestrator/providers/retry-classifier.mjs +325 -0
- package/src/agent/orchestrator/session/abort-lookup.mjs +13 -0
- package/src/agent/orchestrator/session/cache/post-edit-marks.mjs +42 -0
- package/src/agent/orchestrator/session/cache/prefetch-cache.mjs +142 -0
- package/src/agent/orchestrator/session/cache/read-cache.mjs +319 -0
- package/src/agent/orchestrator/session/cache/scoped-cache-outcome.mjs +11 -0
- package/src/agent/orchestrator/session/cache/scoped-cache.mjs +361 -0
- package/src/agent/orchestrator/session/cache/util.mjs +49 -0
- package/src/agent/orchestrator/session/loop.mjs +1478 -0
- package/src/agent/orchestrator/session/manager.mjs +1975 -0
- package/src/agent/orchestrator/session/read-dedup.mjs +6 -0
- package/src/agent/orchestrator/session/result-classification.mjs +65 -0
- package/src/agent/orchestrator/session/save-session-worker.mjs +18 -0
- package/src/agent/orchestrator/session/store.mjs +624 -0
- package/src/agent/orchestrator/session/stream-watchdog.mjs +130 -0
- package/src/agent/orchestrator/session/tool-result-offload.mjs +166 -0
- package/src/agent/orchestrator/session/trim.mjs +491 -0
- package/src/agent/orchestrator/smart-bridge/CACHE-SHARD.md +115 -0
- package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +327 -0
- package/src/agent/orchestrator/smart-bridge/cache-obs.mjs +150 -0
- package/src/agent/orchestrator/smart-bridge/cache-strategy.mjs +228 -0
- package/src/agent/orchestrator/smart-bridge/index.mjs +215 -0
- package/src/agent/orchestrator/smart-bridge/profiles.mjs +37 -0
- package/src/agent/orchestrator/smart-bridge/registry.mjs +348 -0
- package/src/agent/orchestrator/smart-bridge/session-builder.mjs +116 -0
- package/src/agent/orchestrator/stall-policy.mjs +195 -0
- package/src/agent/orchestrator/tool-loop-guard.mjs +75 -0
- package/src/agent/orchestrator/tools/bash-policy-scan.mjs +77 -0
- package/src/agent/orchestrator/tools/bash-session.mjs +721 -0
- package/src/agent/orchestrator/tools/builtin/advisory-lock.mjs +171 -0
- package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +455 -0
- package/src/agent/orchestrator/tools/builtin/atomic-write.mjs +236 -0
- package/src/agent/orchestrator/tools/builtin/bash-tool.mjs +480 -0
- package/src/agent/orchestrator/tools/builtin/binary-file.mjs +76 -0
- package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +256 -0
- package/src/agent/orchestrator/tools/builtin/cache-layers.mjs +386 -0
- package/src/agent/orchestrator/tools/builtin/cwd-utils.mjs +37 -0
- package/src/agent/orchestrator/tools/builtin/device-paths.mjs +154 -0
- package/src/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +292 -0
- package/src/agent/orchestrator/tools/builtin/diff-utils.mjs +109 -0
- package/src/agent/orchestrator/tools/builtin/edit-base-guard.mjs +58 -0
- package/src/agent/orchestrator/tools/builtin/edit-byte-plan.mjs +240 -0
- package/src/agent/orchestrator/tools/builtin/edit-byte-utils.mjs +113 -0
- package/src/agent/orchestrator/tools/builtin/edit-commit.mjs +74 -0
- package/src/agent/orchestrator/tools/builtin/edit-context-utils.mjs +242 -0
- package/src/agent/orchestrator/tools/builtin/edit-diagnostics.mjs +211 -0
- package/src/agent/orchestrator/tools/builtin/edit-engine.mjs +1364 -0
- package/src/agent/orchestrator/tools/builtin/edit-failure-context.mjs +126 -0
- package/src/agent/orchestrator/tools/builtin/edit-hint.mjs +141 -0
- package/src/agent/orchestrator/tools/builtin/edit-match-utils.mjs +194 -0
- package/src/agent/orchestrator/tools/builtin/edit-partial-write.mjs +60 -0
- package/src/agent/orchestrator/tools/builtin/edit-stale-refresh.mjs +168 -0
- package/src/agent/orchestrator/tools/builtin/edit-tool.mjs +173 -0
- package/src/agent/orchestrator/tools/builtin/edit-utf8-guard.mjs +48 -0
- package/src/agent/orchestrator/tools/builtin/fs-reachability.mjs +48 -0
- package/src/agent/orchestrator/tools/builtin/fuzzy-match.mjs +99 -0
- package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +170 -0
- package/src/agent/orchestrator/tools/builtin/grep-formatting.mjs +113 -0
- package/src/agent/orchestrator/tools/builtin/hash-utils.mjs +6 -0
- package/src/agent/orchestrator/tools/builtin/list-formatting.mjs +7 -0
- package/src/agent/orchestrator/tools/builtin/list-tool.mjs +593 -0
- package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +89 -0
- package/src/agent/orchestrator/tools/builtin/notebook-edit-tool.mjs +300 -0
- package/src/agent/orchestrator/tools/builtin/open-config-tool.mjs +26 -0
- package/src/agent/orchestrator/tools/builtin/path-diagnostics.mjs +152 -0
- package/src/agent/orchestrator/tools/builtin/path-locks.mjs +35 -0
- package/src/agent/orchestrator/tools/builtin/path-utils.mjs +201 -0
- package/src/agent/orchestrator/tools/builtin/read-args.mjs +103 -0
- package/src/agent/orchestrator/tools/builtin/read-batch.mjs +172 -0
- package/src/agent/orchestrator/tools/builtin/read-constants.mjs +40 -0
- package/src/agent/orchestrator/tools/builtin/read-formatting.mjs +118 -0
- package/src/agent/orchestrator/tools/builtin/read-image-resize.mjs +189 -0
- package/src/agent/orchestrator/tools/builtin/read-image.mjs +88 -0
- package/src/agent/orchestrator/tools/builtin/read-lines.mjs +12 -0
- package/src/agent/orchestrator/tools/builtin/read-mode-tool.mjs +455 -0
- package/src/agent/orchestrator/tools/builtin/read-open.mjs +190 -0
- package/src/agent/orchestrator/tools/builtin/read-range-index.mjs +271 -0
- package/src/agent/orchestrator/tools/builtin/read-ranges.mjs +26 -0
- package/src/agent/orchestrator/tools/builtin/read-single-tool.mjs +728 -0
- package/src/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +173 -0
- package/src/agent/orchestrator/tools/builtin/read-special-files.mjs +268 -0
- package/src/agent/orchestrator/tools/builtin/read-streaming.mjs +602 -0
- package/src/agent/orchestrator/tools/builtin/read-tool.mjs +530 -0
- package/src/agent/orchestrator/tools/builtin/read-windows.mjs +107 -0
- package/src/agent/orchestrator/tools/builtin/rename-tool.mjs +196 -0
- package/src/agent/orchestrator/tools/builtin/rg-runner.mjs +422 -0
- package/src/agent/orchestrator/tools/builtin/search-builders.mjs +158 -0
- package/src/agent/orchestrator/tools/builtin/search-tool.mjs +869 -0
- package/src/agent/orchestrator/tools/builtin/shell-analysis.mjs +653 -0
- package/src/agent/orchestrator/tools/builtin/shell-jobs.mjs +936 -0
- package/src/agent/orchestrator/tools/builtin/shell-output.mjs +36 -0
- package/src/agent/orchestrator/tools/builtin/shell-runtime.mjs +214 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +143 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-store.mjs +206 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-validation.mjs +98 -0
- package/src/agent/orchestrator/tools/builtin/text-stats.mjs +69 -0
- package/src/agent/orchestrator/tools/builtin/windows-roots.mjs +23 -0
- package/src/agent/orchestrator/tools/builtin/write-tool.mjs +401 -0
- package/src/agent/orchestrator/tools/builtin.mjs +500 -0
- package/src/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +39 -0
- package/src/agent/orchestrator/tools/code-graph-tool-defs.mjs +24 -0
- package/src/agent/orchestrator/tools/code-graph.mjs +4095 -0
- package/src/agent/orchestrator/tools/cwd-tool.mjs +298 -0
- package/src/agent/orchestrator/tools/destructive-warning.mjs +323 -0
- package/src/agent/orchestrator/tools/edit-normalize.mjs +603 -0
- package/src/agent/orchestrator/tools/env-scrub.mjs +100 -0
- package/src/agent/orchestrator/tools/graph-binary-fetcher.mjs +144 -0
- package/src/agent/orchestrator/tools/graph-manifest.json +26 -0
- package/src/agent/orchestrator/tools/host-input.mjs +204 -0
- package/src/agent/orchestrator/tools/mutation-content-cache.mjs +67 -0
- package/src/agent/orchestrator/tools/mutation-planner.mjs +75 -0
- package/src/agent/orchestrator/tools/next-call-utils.mjs +48 -0
- package/src/agent/orchestrator/tools/patch-binary-fetcher.mjs +133 -0
- package/src/agent/orchestrator/tools/patch-manifest.json +26 -0
- package/src/agent/orchestrator/tools/patch-tool-defs.mjs +20 -0
- package/src/agent/orchestrator/tools/patch.mjs +2754 -0
- package/src/agent/orchestrator/tools/progress-message.mjs +118 -0
- package/src/agent/orchestrator/tools/result-compression.mjs +279 -0
- package/src/agent/orchestrator/tools/shell-command.mjs +865 -0
- package/src/agent/orchestrator/tools/shell-exec-policy.mjs +89 -0
- package/src/agent/orchestrator/tools/shell-policy-danger-target.mjs +27 -0
- package/src/agent/orchestrator/tools/shell-policy-imports.mjs +7 -0
- package/src/agent/orchestrator/tools/shell-policy.mjs +345 -0
- package/src/agent/orchestrator/tools/shell-snapshot.mjs +313 -0
- package/src/agent/orchestrator/workflow-store.mjs +93 -0
- package/src/agent/tool-defs.mjs +103 -0
- package/src/channels/backends/discord.mjs +784 -0
- package/src/channels/data/voice-runtime-manifest.json +138 -0
- package/src/channels/index.mjs +3229 -0
- package/src/channels/lib/cli-worker-host.mjs +12 -0
- package/src/channels/lib/config-lock.mjs +13 -0
- package/src/channels/lib/config.mjs +292 -0
- package/src/channels/lib/drop-trace.mjs +71 -0
- package/src/channels/lib/event-pipeline.mjs +81 -0
- package/src/channels/lib/event-queue.mjs +345 -0
- package/src/channels/lib/executor.mjs +168 -0
- package/src/channels/lib/format.mjs +188 -0
- package/src/channels/lib/holidays.mjs +138 -0
- package/src/channels/lib/hook-pipe-server.mjs +802 -0
- package/src/channels/lib/interaction-workflows.mjs +184 -0
- package/src/channels/lib/memory-client.mjs +149 -0
- package/src/channels/lib/output-forwarder.mjs +765 -0
- package/src/channels/lib/runtime-paths.mjs +479 -0
- package/src/channels/lib/scheduler.mjs +723 -0
- package/src/channels/lib/session-control.mjs +36 -0
- package/src/channels/lib/session-discovery.mjs +103 -0
- package/src/channels/lib/settings.mjs +11 -0
- package/src/channels/lib/state-file.mjs +68 -0
- package/src/channels/lib/status-snapshot.mjs +219 -0
- package/src/channels/lib/tool-format.mjs +140 -0
- package/src/channels/lib/transcript-discovery.mjs +195 -0
- package/src/channels/lib/voice-runtime-fetcher.mjs +734 -0
- package/src/channels/lib/webhook.mjs +1179 -0
- package/src/channels/lib/whisper-server.mjs +477 -0
- package/src/channels/tool-defs.mjs +170 -0
- package/src/daemon/host.mjs +118 -0
- package/src/daemon/mcp-transport.mjs +47 -0
- package/src/daemon/session.mjs +100 -0
- package/src/daemon/thin-client.mjs +71 -0
- package/src/daemon/transport.mjs +163 -0
- package/src/memory/data/runtime-manifest.json +40 -0
- package/src/memory/index.mjs +3305 -0
- package/src/memory/lib/agent-ipc.mjs +93 -0
- package/src/memory/lib/bridge-trace-queries.mjs +120 -0
- package/src/memory/lib/core-memory-store.mjs +330 -0
- package/src/memory/lib/embedding-provider.mjs +269 -0
- package/src/memory/lib/embedding-worker.mjs +323 -0
- package/src/memory/lib/llm-worker-host.mjs +17 -0
- package/src/memory/lib/memory-cycle.mjs +11 -0
- package/src/memory/lib/memory-cycle1.mjs +641 -0
- package/src/memory/lib/memory-cycle2.mjs +1284 -0
- package/src/memory/lib/memory-cycle3.mjs +540 -0
- package/src/memory/lib/memory-embed.mjs +299 -0
- package/src/memory/lib/memory-extraction.mjs +5 -0
- package/src/memory/lib/memory-maintenance-store.mjs +32 -0
- package/src/memory/lib/memory-ops-policy.mjs +190 -0
- package/src/memory/lib/memory-recall-id-patch.mjs +15 -0
- package/src/memory/lib/memory-recall-read-query.mjs +7 -0
- package/src/memory/lib/memory-recall-scope-filter.mjs +63 -0
- package/src/memory/lib/memory-recall-store.mjs +621 -0
- package/src/memory/lib/memory-retrievers.mjs +112 -0
- package/src/memory/lib/memory-score.mjs +71 -0
- package/src/memory/lib/memory-text-utils.mjs +58 -0
- package/src/memory/lib/memory.mjs +412 -0
- package/src/memory/lib/model-profile.mjs +85 -0
- package/src/memory/lib/pg/adapter.mjs +308 -0
- package/src/memory/lib/pg/process.mjs +360 -0
- package/src/memory/lib/pg/supervisor.mjs +396 -0
- package/src/memory/lib/project-id-resolver.mjs +86 -0
- package/src/memory/lib/runtime-fetcher.mjs +442 -0
- package/src/memory/lib/trace-store.mjs +728 -0
- package/src/memory/tool-defs.mjs +79 -0
- package/src/search/index.mjs +1173 -0
- package/src/search/lib/backends/anthropic-oauth.mjs +98 -0
- package/src/search/lib/backends/exa.mjs +50 -0
- package/src/search/lib/backends/firecrawl.mjs +61 -0
- package/src/search/lib/backends/gemini-api.mjs +83 -0
- package/src/search/lib/backends/grok-oauth.mjs +86 -0
- package/src/search/lib/backends/index.mjs +150 -0
- package/src/search/lib/backends/openai-api.mjs +144 -0
- package/src/search/lib/backends/openai-oauth.mjs +98 -0
- package/src/search/lib/backends/openai-web-search.mjs +76 -0
- package/src/search/lib/backends/tavily.mjs +55 -0
- package/src/search/lib/backends/xai-api.mjs +113 -0
- package/src/search/lib/cache.mjs +131 -0
- package/src/search/lib/config.mjs +192 -0
- package/src/search/lib/formatter.mjs +115 -0
- package/src/search/lib/provider-usage.mjs +67 -0
- package/src/search/lib/providers.mjs +47 -0
- package/src/search/lib/search-intent.mjs +109 -0
- package/src/search/lib/setup-handler.mjs +261 -0
- package/src/search/lib/state.mjs +201 -0
- package/src/search/lib/web-tools.mjs +1207 -0
- package/src/search/tool-defs.mjs +83 -0
- package/src/setup/defender-exclusion.mjs +183 -0
- package/src/shared/abort-controller.mjs +15 -0
- package/src/shared/atomic-file.mjs +420 -0
- package/src/shared/config.mjs +350 -0
- package/src/shared/daemon-recycle.mjs +108 -0
- package/src/shared/disable-claude-builtins.mjs +88 -0
- package/src/shared/err-text.mjs +12 -0
- package/src/shared/llm/cost.mjs +66 -0
- package/src/shared/llm/http-agent.mjs +123 -0
- package/src/shared/llm/index.mjs +41 -0
- package/src/shared/llm/pid-cleanup.mjs +27 -0
- package/src/shared/llm/usage-log.mjs +47 -0
- package/src/shared/plugin-paths.mjs +58 -0
- package/src/shared/schedules-store.mjs +70 -0
- package/src/shared/seed.mjs +119 -0
- package/src/shared/user-cwd.mjs +213 -0
- package/src/shared/user-data-guard.mjs +238 -0
- package/src/status/aggregator.mjs +584 -0
- package/src/status/server.mjs +413 -0
- package/tools.json +1653 -0
|
@@ -0,0 +1,1307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI ChatGPT OAuth (Codex) provider.
|
|
3
|
+
*
|
|
4
|
+
* Dispatches over the WebSocket upgrade of chatgpt.com/backend-api/codex/
|
|
5
|
+
* responses (responses_websockets=2026-02-06 beta). Authenticates via PKCE
|
|
6
|
+
* OAuth or reuses ~/.codex/auth.json. Streaming/framing lives in
|
|
7
|
+
* openai-oauth-ws.mjs; this file owns auth, model catalog, request-body
|
|
8
|
+
* shape, and HTTP/SSE fallback when WebSocket transport is unhealthy.
|
|
9
|
+
*/
|
|
10
|
+
import { createServer } from 'http';
|
|
11
|
+
import { randomBytes, createHash } from 'crypto';
|
|
12
|
+
import { readFileSync, existsSync, mkdirSync, statSync } from 'fs';
|
|
13
|
+
import { join } from 'path';
|
|
14
|
+
import { homedir } from 'os';
|
|
15
|
+
import { getPluginData } from '../config.mjs';
|
|
16
|
+
import { enrichModels } from './model-catalog.mjs';
|
|
17
|
+
import { writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
|
|
18
|
+
|
|
19
|
+
import { sendViaWebSocket } from './openai-oauth-ws.mjs';
|
|
20
|
+
import { resolveProviderCacheKey } from '../smart-bridge/cache-strategy.mjs';
|
|
21
|
+
import {
|
|
22
|
+
appendBridgeTrace,
|
|
23
|
+
traceBridgeFetch,
|
|
24
|
+
traceBridgeSse,
|
|
25
|
+
traceBridgeUsage,
|
|
26
|
+
} from '../bridge-trace.mjs';
|
|
27
|
+
import {
|
|
28
|
+
PROVIDER_GENERATE_TOTAL_TIMEOUT_MS,
|
|
29
|
+
PROVIDER_HTTP_RESPONSE_TIMEOUT_MS,
|
|
30
|
+
createTimeoutSignal,
|
|
31
|
+
} from '../stall-policy.mjs';
|
|
32
|
+
import { populateHttpStatusFromMessage } from './retry-classifier.mjs';
|
|
33
|
+
import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
|
|
34
|
+
// --- Constants ---
|
|
35
|
+
const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
36
|
+
const TOKEN_URL = 'https://auth.openai.com/oauth/token';
|
|
37
|
+
const CODEX_RESPONSES_URL = 'https://chatgpt.com/backend-api/codex/responses';
|
|
38
|
+
// Version string baked into the models endpoint query — Codex rejects the
|
|
39
|
+
// request without it. Keep close to the latest published Codex CLI because
|
|
40
|
+
// older versions trigger a visibility-filtered catalog (e.g. only rollout
|
|
41
|
+
// models). Bump when the real CLI bumps.
|
|
42
|
+
// Codex backend gates new model exposures (e.g. gpt-5.5 only on >= 0.130.0)
|
|
43
|
+
// on the client_version header. Resolve dynamically from npm so newly-shipped
|
|
44
|
+
// models surface within a day instead of waiting on a hardcoded bump here.
|
|
45
|
+
// Cached 24h in-process; npm failure falls back to the floor below.
|
|
46
|
+
const CODEX_CLIENT_VERSION_FLOOR = '0.130.0';
|
|
47
|
+
const CODEX_VERSION_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
48
|
+
let _codexVersionCache = { value: null, fetchedAt: 0 };
|
|
49
|
+
|
|
50
|
+
async function _resolveCodexClientVersion() {
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
if (_codexVersionCache.value && now - _codexVersionCache.fetchedAt < CODEX_VERSION_CACHE_TTL_MS) {
|
|
53
|
+
return _codexVersionCache.value;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const res = await fetch('https://registry.npmjs.org/@openai/codex/latest', {
|
|
57
|
+
signal: AbortSignal.timeout(5_000),
|
|
58
|
+
});
|
|
59
|
+
if (res.ok) {
|
|
60
|
+
const j = await res.json();
|
|
61
|
+
const v = String(j?.version || '').trim();
|
|
62
|
+
if (/^\d+\.\d+\.\d+/.test(v)) {
|
|
63
|
+
_codexVersionCache = { value: v, fetchedAt: now };
|
|
64
|
+
return v;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch { /* network down / npm rejects — use floor */ }
|
|
68
|
+
_codexVersionCache = { value: CODEX_CLIENT_VERSION_FLOOR, fetchedAt: now };
|
|
69
|
+
return CODEX_CLIENT_VERSION_FLOOR;
|
|
70
|
+
}
|
|
71
|
+
const CODEX_MODEL_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
72
|
+
const TOKEN_REFRESH_SKEW_MS = 5 * 60_000;
|
|
73
|
+
|
|
74
|
+
function _codexModelCachePath() {
|
|
75
|
+
return join(getPluginData(), 'openai-oauth-models.json');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function _loadCodexModelCache() {
|
|
79
|
+
const path = _codexModelCachePath();
|
|
80
|
+
if (!existsSync(path)) return null;
|
|
81
|
+
try {
|
|
82
|
+
const raw = JSON.parse(readFileSync(path, 'utf-8'));
|
|
83
|
+
if (!raw?.fetchedAt || !Array.isArray(raw.models)) return null;
|
|
84
|
+
if (Date.now() - raw.fetchedAt > CODEX_MODEL_CACHE_TTL_MS) return null;
|
|
85
|
+
return raw.models;
|
|
86
|
+
} catch { return null; }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function _saveCodexModelCache(models) {
|
|
90
|
+
try {
|
|
91
|
+
writeJsonAtomicSync(_codexModelCachePath(), {
|
|
92
|
+
fetchedAt: Date.now(),
|
|
93
|
+
models,
|
|
94
|
+
}, { lock: true, fsyncDir: true });
|
|
95
|
+
_inMemoryCodexCatalog = Array.isArray(models) ? models.slice() : null;
|
|
96
|
+
} catch { /* best-effort */ }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// In-memory mirror of the on-disk catalog, same pattern as anthropic-oauth.
|
|
100
|
+
// Populated on first listModels() and after every _saveCodexModelCache.
|
|
101
|
+
let _inMemoryCodexCatalog = null;
|
|
102
|
+
let _codexRefreshInFlight = null;
|
|
103
|
+
let _oauthRefreshInFlight = null;
|
|
104
|
+
let _lastCodexListModelsError = '';
|
|
105
|
+
|
|
106
|
+
export function getOpenAIOAuthModelCatalogError() {
|
|
107
|
+
return _lastCodexListModelsError;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function _codexCatalogHas(id) {
|
|
111
|
+
if (!id || !Array.isArray(_inMemoryCodexCatalog)) return false;
|
|
112
|
+
return _inMemoryCodexCatalog.some(m => m.id === id);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Codex returns dated ids (gpt-5.4-mini-2026-03-17). Strip the trailing
|
|
116
|
+
// -YYYY-MM-DD to get the version alias (gpt-5.4-mini). Unknown shapes pass
|
|
117
|
+
// through unchanged.
|
|
118
|
+
function _displayCodexModel(id) {
|
|
119
|
+
if (!id || typeof id !== 'string') return id;
|
|
120
|
+
return id.replace(/-\d{4}-\d{2}-\d{2}$/, '');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function _normalizeCodexModel(m) {
|
|
124
|
+
const id = m?.slug || m?.id;
|
|
125
|
+
const family = _codexFamily(id);
|
|
126
|
+
// Codex doesn't use dated ids — everything is effectively a version alias.
|
|
127
|
+
return {
|
|
128
|
+
id,
|
|
129
|
+
name: m?.display_name || id,
|
|
130
|
+
display: m?.display_name || id,
|
|
131
|
+
family,
|
|
132
|
+
provider: 'openai-oauth',
|
|
133
|
+
contextWindow: m?.context_window || 1000000,
|
|
134
|
+
outputTokens: m?.auto_compact_token_limit || 32768,
|
|
135
|
+
tier: 'version',
|
|
136
|
+
latest: false,
|
|
137
|
+
description: m?.description || '',
|
|
138
|
+
reasoningLevels: (m?.supported_reasoning_levels || []).map(r => r.effort),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function _codexFamily(id) {
|
|
143
|
+
const s = String(id || '').toLowerCase();
|
|
144
|
+
if (s.includes('nano')) return 'gpt-nano';
|
|
145
|
+
if (s.includes('mini')) return 'gpt-mini';
|
|
146
|
+
if (s.includes('codex')) return 'gpt-codex';
|
|
147
|
+
if (s.startsWith('gpt-5.5')) return 'gpt-5.5';
|
|
148
|
+
if (s.startsWith('gpt-5.4')) return 'gpt-5.4';
|
|
149
|
+
if (s.startsWith('gpt-5.2')) return 'gpt-5.2';
|
|
150
|
+
if (s.startsWith('gpt-5')) return 'gpt-5';
|
|
151
|
+
return 'gpt';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Compare two Codex ids by the X.Y version embedded in `gpt-X.Y`. Mirrors
|
|
155
|
+
// anthropic-oauth's _compareVersion, but Codex ids have no trailing date so
|
|
156
|
+
// the version lives in the dotted number, not a -YYYY-MM-DD suffix.
|
|
157
|
+
function _compareVersion(a, b) {
|
|
158
|
+
const na = (String(a).match(/gpt-(\d+)\.(\d+)/) || []).slice(1).map(Number);
|
|
159
|
+
const nb = (String(b).match(/gpt-(\d+)\.(\d+)/) || []).slice(1).map(Number);
|
|
160
|
+
for (let i = 0; i < Math.max(na.length, nb.length); i++) {
|
|
161
|
+
if ((na[i] || 0) !== (nb[i] || 0)) return (na[i] || 0) - (nb[i] || 0);
|
|
162
|
+
}
|
|
163
|
+
return String(a).localeCompare(String(b));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Main gpt-5 chat family only: exclude the mini/nano/codex variants so "latest"
|
|
167
|
+
// resolves to the flagship, not a smaller sibling.
|
|
168
|
+
function _isMainCodexFamily(family) {
|
|
169
|
+
return typeof family === 'string' && family.startsWith('gpt-5');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Mark the highest-version model per family as `latest: true`. VERSION-based
|
|
173
|
+
// (Codex ids carry no `created`), mirroring anthropic-oauth's per-family pass.
|
|
174
|
+
function _markLatestCodex(models) {
|
|
175
|
+
const byFamily = new Map();
|
|
176
|
+
for (const m of models) {
|
|
177
|
+
if (!m?.id) continue;
|
|
178
|
+
const cur = byFamily.get(m.family);
|
|
179
|
+
if (!cur || _compareVersion(m.id, cur.id) > 0) {
|
|
180
|
+
byFamily.set(m.family, m);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
for (const m of byFamily.values()) m.latest = true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Newest MAIN gpt-5 chat model by version, read from the SYNC in-memory
|
|
187
|
+
// catalog mirror. Returns null until populated; callers warm via
|
|
188
|
+
// ensureLatestCodexModel when null.
|
|
189
|
+
export function resolveLatestCodexModel() {
|
|
190
|
+
if (!Array.isArray(_inMemoryCodexCatalog)) return null;
|
|
191
|
+
let best = null;
|
|
192
|
+
for (const m of _inMemoryCodexCatalog) {
|
|
193
|
+
if (!m?.id || !_isMainCodexFamily(m.family)) continue;
|
|
194
|
+
if (!best || _compareVersion(m.id, best.id) > 0) best = m;
|
|
195
|
+
}
|
|
196
|
+
return best?.id || null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function ensureLatestCodexModel(provider) {
|
|
200
|
+
let m = resolveLatestCodexModel();
|
|
201
|
+
if (m) return m;
|
|
202
|
+
await provider._refreshModelCache();
|
|
203
|
+
m = resolveLatestCodexModel();
|
|
204
|
+
if (m) return m;
|
|
205
|
+
throw new Error('[openai-oauth] model catalog unavailable after warmup — cannot resolve default model');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function getOwnTokenPath() {
|
|
209
|
+
const dir = getPluginData();
|
|
210
|
+
if (!existsSync(dir))
|
|
211
|
+
mkdirSync(dir, { recursive: true });
|
|
212
|
+
return join(dir, 'openai-oauth.json');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Public predicate used by config.buildDefaultConfig — provider is enabled
|
|
216
|
+
// when own tokens exist OR codex bootstrap auth is present. Single truth:
|
|
217
|
+
// same loader the runtime uses (loadTokens), no parallel hard-coded path probe.
|
|
218
|
+
export function hasOpenAIOAuthCredentials() {
|
|
219
|
+
try {
|
|
220
|
+
const tokens = loadTokens();
|
|
221
|
+
return !!(tokens?.access_token && tokens?.refresh_token);
|
|
222
|
+
} catch { return false; }
|
|
223
|
+
}
|
|
224
|
+
function _normalizeExpiresAt(value) {
|
|
225
|
+
const n = Number(value || 0);
|
|
226
|
+
if (!Number.isFinite(n) || n <= 0) return 0;
|
|
227
|
+
return n < 1e12 ? n * 1000 : n;
|
|
228
|
+
}
|
|
229
|
+
function _tokensMaxMtime() {
|
|
230
|
+
let max = 0;
|
|
231
|
+
const paths = [getOwnTokenPath(), join(homedir(), '.codex', 'auth.json')];
|
|
232
|
+
for (const p of paths) {
|
|
233
|
+
try {
|
|
234
|
+
const s = statSync(p);
|
|
235
|
+
if (s.mtimeMs > max) max = s.mtimeMs;
|
|
236
|
+
} catch { /* not present — skip */ }
|
|
237
|
+
}
|
|
238
|
+
return max;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function _codexCliAuthPath() {
|
|
242
|
+
return join(homedir(), '.codex', 'auth.json');
|
|
243
|
+
}
|
|
244
|
+
function _loadOwnCodexTokens() {
|
|
245
|
+
const ownPath = getOwnTokenPath();
|
|
246
|
+
if (!existsSync(ownPath)) return null;
|
|
247
|
+
try {
|
|
248
|
+
const stat = statSync(ownPath);
|
|
249
|
+
const own = JSON.parse(readFileSync(ownPath, 'utf-8'));
|
|
250
|
+
if (own.access_token && own.refresh_token) {
|
|
251
|
+
return {
|
|
252
|
+
...own,
|
|
253
|
+
expires_at: _normalizeExpiresAt(own.expires_at ?? own.expiresAt) || _expiryFromAccessToken(own.access_token),
|
|
254
|
+
account_id: own.account_id || extractAccountId(own.access_token),
|
|
255
|
+
_mtimeMs: stat.mtimeMs,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch { /* fall through */ }
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
function _loadCodexCliTokens() {
|
|
263
|
+
const codexPath = _codexCliAuthPath();
|
|
264
|
+
if (!existsSync(codexPath)) return null;
|
|
265
|
+
try {
|
|
266
|
+
const stat = statSync(codexPath);
|
|
267
|
+
const data = JSON.parse(readFileSync(codexPath, 'utf-8'));
|
|
268
|
+
const tokens = data.tokens || data;
|
|
269
|
+
if (tokens.access_token && tokens.refresh_token) {
|
|
270
|
+
const expiresAt = _normalizeExpiresAt(data.expires_at ?? tokens.expires_at ?? data.expiresAt ?? tokens.expiresAt) || _expiryFromAccessToken(tokens.access_token);
|
|
271
|
+
return {
|
|
272
|
+
access_token: tokens.access_token,
|
|
273
|
+
refresh_token: tokens.refresh_token,
|
|
274
|
+
expires_at: expiresAt,
|
|
275
|
+
account_id: tokens.account_id || extractAccountId(tokens.access_token),
|
|
276
|
+
_mtimeMs: stat.mtimeMs,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
catch { /* fall through */ }
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
// Own store is authoritative (accurate expires_at from refresh); the Codex CLI
|
|
284
|
+
// store seeds the initial bootstrap. But the refresh-token lineage is shared
|
|
285
|
+
// single-use with the Codex CLI, so when the CLI store is STRICTLY newer on
|
|
286
|
+
// disk (an independent `codex login`/CLI refresh) we must adopt it instead of
|
|
287
|
+
// replaying our consumed token. Freshest-wins, own preferred on a tie.
|
|
288
|
+
function loadTokens() {
|
|
289
|
+
const own = _loadOwnCodexTokens();
|
|
290
|
+
const cli = _loadCodexCliTokens();
|
|
291
|
+
if (own && cli) return (cli._mtimeMs > own._mtimeMs) ? cli : own;
|
|
292
|
+
return own || cli;
|
|
293
|
+
}
|
|
294
|
+
function saveTokens(tokens) {
|
|
295
|
+
const target = getOwnTokenPath();
|
|
296
|
+
writeJsonAtomicSync(target, tokens, { lock: true, fsyncDir: true, mode: 0o600, secret: true });
|
|
297
|
+
}
|
|
298
|
+
// Write rotated tokens back to the Codex CLI store (~/.codex/auth.json) so the
|
|
299
|
+
// Codex CLI picks up the rotation instead of replaying a consumed refresh_token
|
|
300
|
+
// from the shared single-use lineage. Mirrors anthropic-oauth's write-back.
|
|
301
|
+
// Best-effort; the own store stays authoritative. Host-owned file: preserve all
|
|
302
|
+
// other fields and don't re-permission it (no secret/mode).
|
|
303
|
+
function _writeBackCodexCliTokens(tokens) {
|
|
304
|
+
const path = _codexCliAuthPath();
|
|
305
|
+
if (!existsSync(path)) return;
|
|
306
|
+
try {
|
|
307
|
+
const raw = JSON.parse(readFileSync(path, 'utf-8'));
|
|
308
|
+
if (!raw || typeof raw !== 'object') return;
|
|
309
|
+
const slot = (raw.tokens && typeof raw.tokens === 'object') ? raw.tokens : raw;
|
|
310
|
+
slot.access_token = tokens.access_token;
|
|
311
|
+
slot.refresh_token = tokens.refresh_token;
|
|
312
|
+
raw.last_refresh = new Date().toISOString();
|
|
313
|
+
// Preserve the Codex CLI file's existing POSIX mode (writeJsonAtomicSync
|
|
314
|
+
// otherwise defaults to 0o600, re-permissioning a host-owned file).
|
|
315
|
+
let mode;
|
|
316
|
+
try { mode = statSync(path).mode & 0o777; } catch { /* keep helper default */ }
|
|
317
|
+
writeJsonAtomicSync(path, raw, { lock: true, fsyncDir: true, mode });
|
|
318
|
+
} catch (err) {
|
|
319
|
+
process.stderr.write(`[openai-oauth] Codex CLI store write-back failed: ${String(err?.message || err).slice(0, 200)}\n`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function extractAccountId(token) {
|
|
323
|
+
try {
|
|
324
|
+
const parts = token.split('.');
|
|
325
|
+
if (parts.length !== 3)
|
|
326
|
+
return undefined;
|
|
327
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString('utf-8'));
|
|
328
|
+
return payload?.['https://api.openai.com/auth']?.chatgpt_account_id;
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// Derive token expiry from the access_token's JWT `exp` claim (epoch ms), as a
|
|
335
|
+
// fallback when the source store carries no explicit expires_at — e.g. the Codex
|
|
336
|
+
// CLI's ~/.codex/auth.json records only last_refresh, so expires_at resolves to 0
|
|
337
|
+
// and ensureAuth reads that as "never expires", disabling proactive refresh; the
|
|
338
|
+
// token then only refreshes reactively after a request fails (and a WS handshake
|
|
339
|
+
// 401 can surface as an opaque transport error that the 401 path misses). Returns
|
|
340
|
+
// 0 for opaque (non-JWT) tokens. JWT `exp` is epoch SECONDS (RFC 7519).
|
|
341
|
+
function _expiryFromAccessToken(token) {
|
|
342
|
+
try {
|
|
343
|
+
const parts = String(token || '').split('.');
|
|
344
|
+
if (parts.length !== 3) return 0;
|
|
345
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString('utf-8'));
|
|
346
|
+
const exp = Number(payload?.exp);
|
|
347
|
+
return Number.isFinite(exp) && exp > 0 ? exp * 1000 : 0;
|
|
348
|
+
}
|
|
349
|
+
catch { return 0; }
|
|
350
|
+
}
|
|
351
|
+
// --- Token refresh ---
|
|
352
|
+
async function refreshTokens(refreshToken) {
|
|
353
|
+
const controller = new AbortController();
|
|
354
|
+
const timeout = setTimeout(() => controller.abort(), 30_000);
|
|
355
|
+
try {
|
|
356
|
+
const res = await fetch(TOKEN_URL, {
|
|
357
|
+
method: 'POST',
|
|
358
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
359
|
+
body: new URLSearchParams({
|
|
360
|
+
grant_type: 'refresh_token',
|
|
361
|
+
refresh_token: refreshToken,
|
|
362
|
+
client_id: CLIENT_ID,
|
|
363
|
+
}),
|
|
364
|
+
// Never follow a redirect on a secret-bearing request: a token
|
|
365
|
+
// endpoint that 307/308-redirects would replay the refresh_token to
|
|
366
|
+
// the redirect target. Fail loud instead.
|
|
367
|
+
redirect: 'error',
|
|
368
|
+
signal: controller.signal,
|
|
369
|
+
dispatcher: getLlmDispatcher(),
|
|
370
|
+
});
|
|
371
|
+
if (!res.ok) {
|
|
372
|
+
const text = await res.text().catch(() => '');
|
|
373
|
+
// Distinguish a terminally-dead refresh token (consumed by the Codex
|
|
374
|
+
// CLI's single-use lineage) from transient failures, so the caller can
|
|
375
|
+
// re-read disk and retry once with a newer token instead of
|
|
376
|
+
// collapsing every failure to a generic null.
|
|
377
|
+
if (res.status === 400 || res.status === 401 || /invalid_grant|revoked|reused/i.test(text)) {
|
|
378
|
+
throw Object.assign(new Error(`OpenAI OAuth token refresh ${res.status} (invalid_grant)`), { isInvalidGrant: true });
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
const json = await res.json();
|
|
383
|
+
if (!json.access_token) return null;
|
|
384
|
+
const expiresAt = _normalizeExpiresAt(json.expires_at ?? json.expiresAt)
|
|
385
|
+
|| (typeof json.expires_in === 'number' ? Date.now() + json.expires_in * 1000 : 0);
|
|
386
|
+
const tokens = {
|
|
387
|
+
access_token: json.access_token,
|
|
388
|
+
refresh_token: json.refresh_token || refreshToken,
|
|
389
|
+
expires_at: expiresAt,
|
|
390
|
+
account_id: extractAccountId(json.access_token),
|
|
391
|
+
};
|
|
392
|
+
// CLI store first, own store last: the own store keeps the newest mtime
|
|
393
|
+
// (and its accurate refresh expires_at), so freshest-wins loadTokens
|
|
394
|
+
// treats our refresh as authoritative while the CLI still picks up the
|
|
395
|
+
// rotated token.
|
|
396
|
+
_writeBackCodexCliTokens(tokens);
|
|
397
|
+
saveTokens(tokens);
|
|
398
|
+
return tokens;
|
|
399
|
+
} catch (err) {
|
|
400
|
+
if (err?.name === 'AbortError')
|
|
401
|
+
throw new Error('OpenAI OAuth token refresh timed out after 30000ms');
|
|
402
|
+
throw err;
|
|
403
|
+
} finally {
|
|
404
|
+
clearTimeout(timeout);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// --- Build Responses API request ---
|
|
408
|
+
/**
|
|
409
|
+
* Convert a message slice to Responses API input items.
|
|
410
|
+
*/
|
|
411
|
+
function convertMessagesToResponsesInput(messages) {
|
|
412
|
+
const out = [];
|
|
413
|
+
for (const m of messages) {
|
|
414
|
+
if (!m || m.role === 'system') continue;
|
|
415
|
+
if (m.role === 'tool') {
|
|
416
|
+
out.push({
|
|
417
|
+
type: 'function_call_output',
|
|
418
|
+
call_id: m.toolCallId || '',
|
|
419
|
+
output: m.content,
|
|
420
|
+
});
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (m.role === 'assistant' && Array.isArray(m.toolCalls) && m.toolCalls.length) {
|
|
424
|
+
// Reasoning replay deliberately omitted: Codex rejects an
|
|
425
|
+
// `rs_*` reasoning item with the same id across the same
|
|
426
|
+
// handshake session_id (in-memory conversation state lives
|
|
427
|
+
// for the WS_IDLE_MS window even after a socket close).
|
|
428
|
+
// Server-side state already preserves the prefix; sending
|
|
429
|
+
// reasoning in `input` triggers "Duplicate item".
|
|
430
|
+
if (m.content) out.push({ role: 'assistant', content: m.content });
|
|
431
|
+
for (const tc of m.toolCalls) {
|
|
432
|
+
out.push({
|
|
433
|
+
type: 'function_call',
|
|
434
|
+
call_id: tc.id,
|
|
435
|
+
name: tc.name,
|
|
436
|
+
arguments: JSON.stringify(tc.arguments),
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
out.push({
|
|
442
|
+
role: m.role === 'assistant' ? 'assistant' : 'user',
|
|
443
|
+
content: m.content,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
return out;
|
|
447
|
+
}
|
|
448
|
+
export function buildRequestBody(messages, model, tools, sendOpts) {
|
|
449
|
+
// Extract system/instructions
|
|
450
|
+
const systemMsgs = messages.filter(m => m.role === 'system');
|
|
451
|
+
const instructions = systemMsgs.map(m => m.content).join('\n\n') || 'You are a helpful assistant.';
|
|
452
|
+
const opts = sendOpts || {};
|
|
453
|
+
const input = convertMessagesToResponsesInput(messages);
|
|
454
|
+
// Match the body shape pi-mono and the official Codex CLI ship so the
|
|
455
|
+
// server-side auto-cache routes correctly. text.verbosity / include /
|
|
456
|
+
// tool_choice / parallel_tool_calls are all inert without side effects
|
|
457
|
+
// for most callers but their presence affects how Codex classifies the
|
|
458
|
+
// request (and therefore whether the prompt cache is consulted).
|
|
459
|
+
const body = {
|
|
460
|
+
model,
|
|
461
|
+
instructions,
|
|
462
|
+
input,
|
|
463
|
+
store: process.env.MIXDOG_OAI_STORE === 'true' ? true : false,
|
|
464
|
+
stream: true,
|
|
465
|
+
reasoning: { effort: opts.effort || 'medium' },
|
|
466
|
+
text: { verbosity: 'medium' },
|
|
467
|
+
include: ['reasoning.encrypted_content'],
|
|
468
|
+
tool_choice: opts.toolChoice || 'auto',
|
|
469
|
+
parallel_tool_calls: true,
|
|
470
|
+
};
|
|
471
|
+
// Resolver guarantees a stable shared key (never sessionId) so a fresh
|
|
472
|
+
// session reuses the warm shard — see cache-strategy.resolveProviderCacheKey.
|
|
473
|
+
// Clamp to 64 chars (Responses API caps prompt_cache_key; the old sessionId
|
|
474
|
+
// fallback at 71 chars would 400 before streaming).
|
|
475
|
+
body.prompt_cache_key = String(resolveProviderCacheKey(opts, 'openai-oauth')).slice(0, 64);
|
|
476
|
+
// NOTE: prompt_cache_retention is a public OpenAI Responses API parameter —
|
|
477
|
+
// the Codex endpoint (chatgpt.com/backend-api/codex/responses) returns
|
|
478
|
+
// 400 "Unsupported parameter" when it's included. Re-verified 2026-04-19.
|
|
479
|
+
// Leave cache behavior to the Codex server-side default (in-memory, 5-10
|
|
480
|
+
// min). Callers who want extended retention should use the public OpenAI
|
|
481
|
+
// API provider instead of OAuth.
|
|
482
|
+
if (opts.fast === true) {
|
|
483
|
+
// 'priority' is the only fast-class value the Codex OAuth backend
|
|
484
|
+
// accepts on the wire: 'fast' is hard-rejected ("Unsupported
|
|
485
|
+
// service_tier: fast", probed 2026-06-11), and 'priority' is accepted
|
|
486
|
+
// but downgraded to 'default' unless the account is entitled to
|
|
487
|
+
// priority processing. Keep sending it so entitled accounts benefit.
|
|
488
|
+
body.service_tier = 'priority';
|
|
489
|
+
}
|
|
490
|
+
// Add tools
|
|
491
|
+
if (tools?.length) {
|
|
492
|
+
body.tools = tools.map(t => ({
|
|
493
|
+
type: 'function',
|
|
494
|
+
name: t.name,
|
|
495
|
+
description: t.description,
|
|
496
|
+
parameters: t.inputSchema,
|
|
497
|
+
}));
|
|
498
|
+
}
|
|
499
|
+
return body;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function _envFlag(name, fallback = true) {
|
|
503
|
+
const raw = process.env[name];
|
|
504
|
+
if (raw == null || raw === '') return fallback;
|
|
505
|
+
return !['0', 'false', 'off', 'no'].includes(String(raw).toLowerCase());
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function _parseJsonObject(value) {
|
|
509
|
+
try {
|
|
510
|
+
const parsed = JSON.parse(value || '{}');
|
|
511
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
512
|
+
} catch {
|
|
513
|
+
return {};
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function _extractCachedTokens(usage) {
|
|
518
|
+
const details = usage?.input_tokens_details || usage?.prompt_tokens_details || {};
|
|
519
|
+
return Number(details.cached_tokens ?? details.cached ?? usage?.cached_tokens ?? 0) || 0;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function _sseEventsFromBuffer(buffer) {
|
|
523
|
+
const frames = [];
|
|
524
|
+
let rest = buffer.replace(/\r\n/g, '\n');
|
|
525
|
+
let idx;
|
|
526
|
+
while ((idx = rest.indexOf('\n\n')) >= 0) {
|
|
527
|
+
frames.push(rest.slice(0, idx));
|
|
528
|
+
rest = rest.slice(idx + 2);
|
|
529
|
+
}
|
|
530
|
+
return { frames, rest };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function _parseSseFrame(frame) {
|
|
534
|
+
const lines = String(frame || '').split('\n');
|
|
535
|
+
const data = [];
|
|
536
|
+
for (const line of lines) {
|
|
537
|
+
if (!line || line.startsWith(':')) continue;
|
|
538
|
+
if (line.startsWith('data:')) data.push(line.slice(5).trimStart());
|
|
539
|
+
}
|
|
540
|
+
if (!data.length) return null;
|
|
541
|
+
const raw = data.join('\n').trim();
|
|
542
|
+
if (!raw || raw === '[DONE]') return null;
|
|
543
|
+
try { return JSON.parse(raw); } catch { return null; }
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function _pushOutputTextAnnotations(part, citations, citationKeys) {
|
|
547
|
+
const annotations = Array.isArray(part?.annotations) ? part.annotations : [];
|
|
548
|
+
for (const raw of annotations) {
|
|
549
|
+
const url = raw?.url || raw?.uri || raw?.href || '';
|
|
550
|
+
if (!url || citationKeys.has(url)) continue;
|
|
551
|
+
citationKeys.add(url);
|
|
552
|
+
citations.push({
|
|
553
|
+
title: raw?.title || '',
|
|
554
|
+
url,
|
|
555
|
+
snippet: raw?.snippet || raw?.text || raw?.description || '',
|
|
556
|
+
source: 'openai-oauth',
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function _buildOpenAIHttpFallbackHeaders({ auth, cacheKey }) {
|
|
562
|
+
const headers = {
|
|
563
|
+
Authorization: `Bearer ${auth.access_token}`,
|
|
564
|
+
'Content-Type': 'application/json',
|
|
565
|
+
Accept: 'text/event-stream',
|
|
566
|
+
'OpenAI-Beta': 'responses=experimental',
|
|
567
|
+
originator: 'mixdog',
|
|
568
|
+
'chatgpt-account-id': auth.account_id || '',
|
|
569
|
+
'x-client-request-id': randomBytes(16).toString('hex'),
|
|
570
|
+
};
|
|
571
|
+
if (cacheKey) headers.session_id = String(cacheKey);
|
|
572
|
+
return headers;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function _shouldUseOpenAIHttpFallback(err, externalSignal) {
|
|
576
|
+
if (!_envFlag('MIXDOG_OPENAI_OAUTH_HTTP_FALLBACK', true)) return false;
|
|
577
|
+
if (externalSignal?.aborted) return false;
|
|
578
|
+
const status = Number(err?.httpStatus || err?.status || 0);
|
|
579
|
+
if (status === 401 || status === 403 || status === 404 || status === 429) return false;
|
|
580
|
+
if (status >= 500 && status < 600) return true;
|
|
581
|
+
const code = String(err?.code || '');
|
|
582
|
+
if (['EWSACQUIRETIMEOUT', 'ETIMEDOUT', 'ESOCKETTIMEDOUT', 'ECONNRESET', 'EAI_AGAIN', 'ENOTFOUND', 'EAI_NODATA', 'ECONNREFUSED', 'ENETUNREACH', 'EHOSTUNREACH', 'EPIPE'].includes(code)) {
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
const classifier = String(err?.retryClassifier || err?.midstreamClassifier || '');
|
|
586
|
+
if (['timeout', 'reset', 'dns', 'refused', 'network', 'acquire_timeout', 'http_5xx', 'first_byte_timeout'].includes(classifier)) {
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
589
|
+
if (/^http_5\d\d$/.test(classifier)) return true;
|
|
590
|
+
if (err?.firstByteTimeout) return true;
|
|
591
|
+
const msg = String(err?.message || '');
|
|
592
|
+
return /opening handshake has timed out|socket hang up|acquire timed out|no first server event/i.test(msg);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async function sendViaHttpSse({
|
|
596
|
+
auth,
|
|
597
|
+
body,
|
|
598
|
+
opts,
|
|
599
|
+
onStreamDelta,
|
|
600
|
+
onToolCall,
|
|
601
|
+
onStageChange,
|
|
602
|
+
externalSignal,
|
|
603
|
+
poolKey,
|
|
604
|
+
cacheKey,
|
|
605
|
+
iteration,
|
|
606
|
+
useModel,
|
|
607
|
+
fetchFn = fetch,
|
|
608
|
+
} = {}) {
|
|
609
|
+
const totalTimeout = createTimeoutSignal(
|
|
610
|
+
externalSignal,
|
|
611
|
+
PROVIDER_GENERATE_TOTAL_TIMEOUT_MS,
|
|
612
|
+
'OpenAI OAuth HTTP fallback total',
|
|
613
|
+
);
|
|
614
|
+
const headerTimeout = createTimeoutSignal(
|
|
615
|
+
totalTimeout.signal,
|
|
616
|
+
PROVIDER_HTTP_RESPONSE_TIMEOUT_MS,
|
|
617
|
+
'OpenAI OAuth HTTP fallback initial response',
|
|
618
|
+
);
|
|
619
|
+
const headers = _buildOpenAIHttpFallbackHeaders({ auth, cacheKey });
|
|
620
|
+
const fetchStartedAt = Date.now();
|
|
621
|
+
let response;
|
|
622
|
+
try {
|
|
623
|
+
try { onStageChange?.('requesting'); } catch {}
|
|
624
|
+
response = await fetchFn(CODEX_RESPONSES_URL, {
|
|
625
|
+
method: 'POST',
|
|
626
|
+
headers,
|
|
627
|
+
body: JSON.stringify(body),
|
|
628
|
+
signal: headerTimeout.signal,
|
|
629
|
+
dispatcher: getLlmDispatcher(),
|
|
630
|
+
});
|
|
631
|
+
} catch (err) {
|
|
632
|
+
if (headerTimeout.signal?.aborted && headerTimeout.signal.reason instanceof Error) throw headerTimeout.signal.reason;
|
|
633
|
+
throw err;
|
|
634
|
+
} finally {
|
|
635
|
+
headerTimeout.cleanup();
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
traceBridgeFetch({
|
|
639
|
+
sessionId: poolKey,
|
|
640
|
+
headersMs: Date.now() - fetchStartedAt,
|
|
641
|
+
httpStatus: response.status,
|
|
642
|
+
provider: 'openai-oauth',
|
|
643
|
+
model: useModel,
|
|
644
|
+
transport: 'http',
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
if (!response.ok) {
|
|
648
|
+
const text = await response.text().catch(() => '');
|
|
649
|
+
const err = new Error(`OpenAI OAuth HTTP fallback ${response.status}: ${text.slice(0, 200)}`);
|
|
650
|
+
err.httpStatus = response.status;
|
|
651
|
+
err.headers = response.headers;
|
|
652
|
+
populateHttpStatusFromMessage(err, text);
|
|
653
|
+
totalTimeout.cleanup();
|
|
654
|
+
throw err;
|
|
655
|
+
}
|
|
656
|
+
if (!response.body) {
|
|
657
|
+
totalTimeout.cleanup();
|
|
658
|
+
throw new Error('OpenAI OAuth HTTP fallback returned no response body');
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
try { onStageChange?.('streaming'); } catch {}
|
|
662
|
+
const sseStartedAt = Date.now();
|
|
663
|
+
const reader = response.body.getReader();
|
|
664
|
+
const decoder = new TextDecoder();
|
|
665
|
+
// After headerTimeout.cleanup() the in-flight fetch no longer carries a live
|
|
666
|
+
// signal, so a totalTimeout / external abort that fires during a pending
|
|
667
|
+
// reader.read() would otherwise leave the pooled request hanging. Keep the
|
|
668
|
+
// reader tied to totalTimeout for the whole stream: on abort, cancel the
|
|
669
|
+
// reader so the awaited read() unblocks and the socket is released back to
|
|
670
|
+
// the shared pool instead of leaking. reader.cancel() may resolve the
|
|
671
|
+
// pending read() as {done:true} rather than rejecting, which would let a
|
|
672
|
+
// partial response surface as success — so record the abort reason and
|
|
673
|
+
// re-throw it after the loop unblocks (see below).
|
|
674
|
+
let _streamAbortReason = null;
|
|
675
|
+
let _onTotalAbort = null;
|
|
676
|
+
if (totalTimeout.signal) {
|
|
677
|
+
_onTotalAbort = () => {
|
|
678
|
+
const reason = totalTimeout.signal.reason;
|
|
679
|
+
_streamAbortReason = reason instanceof Error
|
|
680
|
+
? reason
|
|
681
|
+
: new Error('OpenAI OAuth HTTP fallback aborted');
|
|
682
|
+
try { reader.cancel(_streamAbortReason).catch(() => {}); } catch {}
|
|
683
|
+
};
|
|
684
|
+
if (totalTimeout.signal.aborted) _onTotalAbort();
|
|
685
|
+
else totalTimeout.signal.addEventListener('abort', _onTotalAbort, { once: true });
|
|
686
|
+
}
|
|
687
|
+
let buffer = '';
|
|
688
|
+
let content = '';
|
|
689
|
+
let model = '';
|
|
690
|
+
let responseId = '';
|
|
691
|
+
let serviceTier = '';
|
|
692
|
+
let usage = null;
|
|
693
|
+
let ttftMs = null;
|
|
694
|
+
const toolCalls = [];
|
|
695
|
+
const pendingCalls = new Map();
|
|
696
|
+
const reasoningItems = [];
|
|
697
|
+
const citations = [];
|
|
698
|
+
const citationKeys = new Set();
|
|
699
|
+
const webSearchCalls = [];
|
|
700
|
+
const webSearchCallKeys = new Set();
|
|
701
|
+
let completed = false;
|
|
702
|
+
|
|
703
|
+
const pushWebSearchCall = (item) => {
|
|
704
|
+
if (!item || item.type !== 'web_search_call') return;
|
|
705
|
+
const key = item.id || JSON.stringify(item.action || item);
|
|
706
|
+
if (webSearchCallKeys.has(key)) return;
|
|
707
|
+
webSearchCallKeys.add(key);
|
|
708
|
+
webSearchCalls.push({ id: item.id || '', status: item.status || '', action: item.action || null });
|
|
709
|
+
};
|
|
710
|
+
const pushReasoningItem = (item) => {
|
|
711
|
+
if (item?.type === 'reasoning' && item.encrypted_content && !reasoningItems.some(r => r.id === item.id)) {
|
|
712
|
+
reasoningItems.push({
|
|
713
|
+
id: item.id || '',
|
|
714
|
+
encrypted_content: item.encrypted_content,
|
|
715
|
+
summary: Array.isArray(item.summary) ? item.summary : [],
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
const meaningful = () => {
|
|
720
|
+
if (ttftMs == null) ttftMs = Date.now() - sseStartedAt;
|
|
721
|
+
try { onStreamDelta?.(); } catch {}
|
|
722
|
+
};
|
|
723
|
+
const handleEvent = (event) => {
|
|
724
|
+
if (!event || typeof event.type !== 'string') return;
|
|
725
|
+
switch (event.type) {
|
|
726
|
+
case 'response.created':
|
|
727
|
+
if (event.response?.model) model = event.response.model;
|
|
728
|
+
if (event.response?.id) responseId = event.response.id;
|
|
729
|
+
break;
|
|
730
|
+
case 'response.output_text.delta':
|
|
731
|
+
content += event.delta || '';
|
|
732
|
+
meaningful();
|
|
733
|
+
break;
|
|
734
|
+
case 'response.reasoning_text.delta':
|
|
735
|
+
case 'response.reasoning_summary_text.delta':
|
|
736
|
+
meaningful();
|
|
737
|
+
break;
|
|
738
|
+
case 'response.output_item.added':
|
|
739
|
+
if (event.item?.type === 'function_call') {
|
|
740
|
+
pendingCalls.set(event.item.id || '', {
|
|
741
|
+
name: event.item.name || '',
|
|
742
|
+
callId: event.item.call_id || '',
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
break;
|
|
746
|
+
case 'response.function_call_arguments.delta':
|
|
747
|
+
meaningful();
|
|
748
|
+
break;
|
|
749
|
+
case 'response.function_call_arguments.done': {
|
|
750
|
+
const itemId = event.item_id || '';
|
|
751
|
+
const pending = pendingCalls.get(itemId);
|
|
752
|
+
const call = {
|
|
753
|
+
id: pending?.callId || event.call_id || '',
|
|
754
|
+
name: pending?.name || event.name || '',
|
|
755
|
+
arguments: _parseJsonObject(event.arguments),
|
|
756
|
+
_pendingItemId: itemId,
|
|
757
|
+
};
|
|
758
|
+
toolCalls.push(call);
|
|
759
|
+
if (call.id && call.name) {
|
|
760
|
+
delete call._pendingItemId;
|
|
761
|
+
try { onToolCall?.(call); } catch {}
|
|
762
|
+
}
|
|
763
|
+
meaningful();
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
766
|
+
case 'response.output_item.done': {
|
|
767
|
+
const item = event.item || {};
|
|
768
|
+
pushReasoningItem(item);
|
|
769
|
+
pushWebSearchCall(item);
|
|
770
|
+
if (item.type === 'function_call') {
|
|
771
|
+
const tc = toolCalls.find(t => t._pendingItemId === (item.id || ''));
|
|
772
|
+
if (tc) {
|
|
773
|
+
if (!tc.id && item.call_id) tc.id = item.call_id;
|
|
774
|
+
if (!tc.name && item.name) tc.name = item.name;
|
|
775
|
+
if (tc.id && tc.name) {
|
|
776
|
+
delete tc._pendingItemId;
|
|
777
|
+
try { onToolCall?.(tc); } catch {}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
break;
|
|
782
|
+
}
|
|
783
|
+
case 'response.completed': {
|
|
784
|
+
const resp = event.response || {};
|
|
785
|
+
serviceTier = resp.service_tier || resp.serviceTier || serviceTier;
|
|
786
|
+
if (!model && resp.model) model = resp.model;
|
|
787
|
+
if (!responseId && resp.id) responseId = resp.id;
|
|
788
|
+
if (resp.usage) {
|
|
789
|
+
usage = {
|
|
790
|
+
inputTokens: resp.usage.input_tokens || 0,
|
|
791
|
+
outputTokens: resp.usage.output_tokens || 0,
|
|
792
|
+
cachedTokens: _extractCachedTokens(resp.usage),
|
|
793
|
+
promptTokens: resp.usage.input_tokens || 0,
|
|
794
|
+
raw: serviceTier ? { ...resp.usage, service_tier: serviceTier } : resp.usage,
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
for (const item of resp.output || []) {
|
|
798
|
+
if (item.type === 'message') {
|
|
799
|
+
for (const part of item.content || []) {
|
|
800
|
+
if (!content && part.type === 'output_text') content += part.text || '';
|
|
801
|
+
if (part.type === 'output_text') _pushOutputTextAnnotations(part, citations, citationKeys);
|
|
802
|
+
}
|
|
803
|
+
} else if (item.type === 'reasoning') {
|
|
804
|
+
pushReasoningItem(item);
|
|
805
|
+
} else if (item.type === 'web_search_call') {
|
|
806
|
+
pushWebSearchCall(item);
|
|
807
|
+
} else if (item.type === 'function_call') {
|
|
808
|
+
const tc = toolCalls.find(t => t._pendingItemId === (item.id || ''));
|
|
809
|
+
if (tc) {
|
|
810
|
+
if (!tc.id && item.call_id) tc.id = item.call_id;
|
|
811
|
+
if (!tc.name && item.name) tc.name = item.name;
|
|
812
|
+
if (tc.id && tc.name) {
|
|
813
|
+
delete tc._pendingItemId;
|
|
814
|
+
try { onToolCall?.(tc); } catch {}
|
|
815
|
+
}
|
|
816
|
+
} else if (item.call_id && item.name) {
|
|
817
|
+
const call = {
|
|
818
|
+
id: item.call_id,
|
|
819
|
+
name: item.name,
|
|
820
|
+
arguments: _parseJsonObject(item.arguments),
|
|
821
|
+
};
|
|
822
|
+
toolCalls.push(call);
|
|
823
|
+
try { onToolCall?.(call); } catch {}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
completed = true;
|
|
828
|
+
break;
|
|
829
|
+
}
|
|
830
|
+
case 'response.done':
|
|
831
|
+
if (!event.response || event.response.status === 'completed') completed = true;
|
|
832
|
+
else if (event.response.status === 'failed') {
|
|
833
|
+
const msg = event.response?.error?.message || 'response.done failed';
|
|
834
|
+
const err = new Error(`OpenAI OAuth HTTP fallback response.done failed: ${msg}`);
|
|
835
|
+
populateHttpStatusFromMessage(err, msg);
|
|
836
|
+
throw err;
|
|
837
|
+
} else if (event.response.status === 'incomplete') {
|
|
838
|
+
throw new Error(`OpenAI OAuth HTTP fallback response.done incomplete: ${event.response?.incomplete_details?.reason || 'incomplete'}`);
|
|
839
|
+
}
|
|
840
|
+
break;
|
|
841
|
+
case 'response.failed': {
|
|
842
|
+
const msg = event.response?.error?.message || event.error?.message || event.message || 'response.failed';
|
|
843
|
+
const err = new Error(`OpenAI OAuth HTTP fallback response.failed: ${msg}`);
|
|
844
|
+
populateHttpStatusFromMessage(err, msg);
|
|
845
|
+
throw err;
|
|
846
|
+
}
|
|
847
|
+
case 'response.incomplete':
|
|
848
|
+
throw new Error(`OpenAI OAuth HTTP fallback response.incomplete: ${event.response?.incomplete_details?.reason || 'incomplete'}`);
|
|
849
|
+
case 'error': {
|
|
850
|
+
const msg = event.message || event.error?.message || 'unknown';
|
|
851
|
+
const err = new Error(`OpenAI OAuth HTTP fallback error: ${msg}`);
|
|
852
|
+
populateHttpStatusFromMessage(err, msg);
|
|
853
|
+
throw err;
|
|
854
|
+
}
|
|
855
|
+
default:
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
try {
|
|
861
|
+
while (true) {
|
|
862
|
+
if (totalTimeout.signal.aborted) {
|
|
863
|
+
const reason = totalTimeout.signal.reason;
|
|
864
|
+
throw reason instanceof Error ? reason : new Error('OpenAI OAuth HTTP fallback aborted');
|
|
865
|
+
}
|
|
866
|
+
const { value, done } = await reader.read();
|
|
867
|
+
if (done) break;
|
|
868
|
+
buffer += decoder.decode(value, { stream: true });
|
|
869
|
+
const parsed = _sseEventsFromBuffer(buffer);
|
|
870
|
+
buffer = parsed.rest;
|
|
871
|
+
for (const frame of parsed.frames) {
|
|
872
|
+
const event = _parseSseFrame(frame);
|
|
873
|
+
if (event) handleEvent(event);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
// The read() above can unblock via reader.cancel() as {done:true} on an
|
|
877
|
+
// external/total-timeout abort. Surface that as the abort/timeout error
|
|
878
|
+
// instead of treating the partial stream as a successful response.
|
|
879
|
+
if (_streamAbortReason) throw _streamAbortReason;
|
|
880
|
+
buffer += decoder.decode();
|
|
881
|
+
const parsed = _sseEventsFromBuffer(buffer + '\n\n');
|
|
882
|
+
for (const frame of parsed.frames) {
|
|
883
|
+
const event = _parseSseFrame(frame);
|
|
884
|
+
if (event) handleEvent(event);
|
|
885
|
+
}
|
|
886
|
+
} finally {
|
|
887
|
+
try { reader.releaseLock?.(); } catch {}
|
|
888
|
+
if (_onTotalAbort && totalTimeout.signal) {
|
|
889
|
+
try { totalTimeout.signal.removeEventListener('abort', _onTotalAbort); } catch {}
|
|
890
|
+
}
|
|
891
|
+
totalTimeout.cleanup();
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const unresolved = toolCalls.find(t => t._pendingItemId);
|
|
895
|
+
if (unresolved) {
|
|
896
|
+
throw new Error(`OpenAI OAuth HTTP fallback function_call salvage failed: missing call_id/name for item_id=${unresolved._pendingItemId || '?'}`);
|
|
897
|
+
}
|
|
898
|
+
if (!completed && !content && !toolCalls.length) {
|
|
899
|
+
throw new Error('OpenAI OAuth HTTP fallback ended before response.completed');
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const liveModel = model || useModel;
|
|
903
|
+
traceBridgeSse({
|
|
904
|
+
sessionId: poolKey,
|
|
905
|
+
sseParseMs: Date.now() - sseStartedAt,
|
|
906
|
+
ttftMs,
|
|
907
|
+
provider: 'openai-oauth',
|
|
908
|
+
model: liveModel,
|
|
909
|
+
transport: 'sse',
|
|
910
|
+
});
|
|
911
|
+
if (usage) {
|
|
912
|
+
traceBridgeUsage({
|
|
913
|
+
sessionId: poolKey,
|
|
914
|
+
iteration,
|
|
915
|
+
inputTokens: usage.inputTokens || 0,
|
|
916
|
+
outputTokens: usage.outputTokens || 0,
|
|
917
|
+
cachedTokens: usage.cachedTokens || 0,
|
|
918
|
+
promptTokens: usage.promptTokens || 0,
|
|
919
|
+
model: liveModel,
|
|
920
|
+
modelDisplay: _displayCodexModel(liveModel),
|
|
921
|
+
responseId: responseId || null,
|
|
922
|
+
rawUsage: usage.raw || null,
|
|
923
|
+
provider: 'openai-oauth',
|
|
924
|
+
serviceTier,
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
return {
|
|
928
|
+
content,
|
|
929
|
+
model: liveModel,
|
|
930
|
+
reasoningItems: reasoningItems.length ? reasoningItems : undefined,
|
|
931
|
+
toolCalls: toolCalls.length ? toolCalls.map(({ _pendingItemId, ...t }) => t) : undefined,
|
|
932
|
+
citations: citations.length ? citations : undefined,
|
|
933
|
+
webSearchCalls: webSearchCalls.length ? webSearchCalls : undefined,
|
|
934
|
+
usage: usage || undefined,
|
|
935
|
+
responseId: responseId || undefined,
|
|
936
|
+
serviceTier: serviceTier || undefined,
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// --- Provider ---
|
|
941
|
+
export class OpenAIOAuthProvider {
|
|
942
|
+
// OpenAI input_tokens already INCLUDES cached_tokens (cached is a subset),
|
|
943
|
+
// so input alone is the context footprint. See registry.mjs.
|
|
944
|
+
static inputExcludesCache = false;
|
|
945
|
+
name = 'openai-oauth';
|
|
946
|
+
tokens = null;
|
|
947
|
+
_refreshFallbackUntil = 0;
|
|
948
|
+
_forceHttpFallback = false;
|
|
949
|
+
config;
|
|
950
|
+
constructor(config) {
|
|
951
|
+
this.config = config || {};
|
|
952
|
+
this.tokens = loadTokens();
|
|
953
|
+
// Warm a kept-alive socket to the Codex responses API so the first
|
|
954
|
+
// request skips the cold TLS handshake. Best-effort; never throws.
|
|
955
|
+
preconnect('https://chatgpt.com');
|
|
956
|
+
}
|
|
957
|
+
async ensureAuth({ forceRefresh = false, reason = 'preemptive' } = {}) {
|
|
958
|
+
if (!this.tokens) this.tokens = loadTokens();
|
|
959
|
+
if (!this.tokens)
|
|
960
|
+
throw new Error('OpenAI OAuth not authenticated. Run codex login first.');
|
|
961
|
+
// Pick up disk-rotated tokens (codex login, host refresh) the moment
|
|
962
|
+
// the auth file is rewritten — without this, a fresh login is ignored
|
|
963
|
+
// until the in-memory token hits its expiry skew.
|
|
964
|
+
const diskMtime = _tokensMaxMtime();
|
|
965
|
+
// Watermark guards termination: if the newest file on disk isn't loadable
|
|
966
|
+
// (e.g. a logged-out host auth.json beside a valid own store), loadTokens
|
|
967
|
+
// falls back to the older valid store; record the scanned mtime so this
|
|
968
|
+
// check can't re-fire on every ensureAuth().
|
|
969
|
+
if (diskMtime > 0 && diskMtime > (this._lastDiskScan || 0) && diskMtime > (this.tokens._mtimeMs || 0)) {
|
|
970
|
+
const fresh = loadTokens();
|
|
971
|
+
if (fresh?.access_token) {
|
|
972
|
+
this.tokens = fresh;
|
|
973
|
+
this._refreshFallbackUntil = 0;
|
|
974
|
+
process.stderr.write(`[openai-oauth] Reloaded tokens from disk (mtime change)\n`);
|
|
975
|
+
}
|
|
976
|
+
this._lastDiskScan = diskMtime;
|
|
977
|
+
}
|
|
978
|
+
if (!forceRefresh && this._refreshFallbackUntil > Date.now() && this.tokens?.access_token) {
|
|
979
|
+
return this.tokens;
|
|
980
|
+
}
|
|
981
|
+
const expiring = this.tokens.expires_at
|
|
982
|
+
? this.tokens.expires_at < Date.now() + TOKEN_REFRESH_SKEW_MS
|
|
983
|
+
: false;
|
|
984
|
+
if (forceRefresh || expiring) {
|
|
985
|
+
this._refreshFallbackUntil = 0;
|
|
986
|
+
this.tokens = await this._refreshTokens({ force: forceRefresh, reason });
|
|
987
|
+
}
|
|
988
|
+
return this.tokens;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
async _refreshTokens({ force = false, reason = 'preemptive' } = {}) {
|
|
992
|
+
const currentToken = this.tokens?.access_token || null;
|
|
993
|
+
const disk = loadTokens();
|
|
994
|
+
const validAfter = Date.now() + (force ? 0 : TOKEN_REFRESH_SKEW_MS);
|
|
995
|
+
if (disk?.access_token && disk.access_token !== currentToken
|
|
996
|
+
&& (!disk.expires_at || disk.expires_at >= validAfter)) {
|
|
997
|
+
this.tokens = disk;
|
|
998
|
+
process.stderr.write(`[openai-oauth] Reloaded tokens from disk\n`);
|
|
999
|
+
return disk;
|
|
1000
|
+
}
|
|
1001
|
+
if (!this.tokens && disk) this.tokens = disk;
|
|
1002
|
+
|
|
1003
|
+
if (_oauthRefreshInFlight) {
|
|
1004
|
+
const shared = await _oauthRefreshInFlight;
|
|
1005
|
+
this.tokens = shared;
|
|
1006
|
+
if (!force || shared?.access_token !== currentToken) return this.tokens;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const startingTokens = this.tokens || disk;
|
|
1010
|
+
_oauthRefreshInFlight = (async () => {
|
|
1011
|
+
const latest = loadTokens() || startingTokens;
|
|
1012
|
+
const latestValidAfter = Date.now() + (force ? 0 : TOKEN_REFRESH_SKEW_MS);
|
|
1013
|
+
if (latest?.access_token && latest.access_token !== currentToken
|
|
1014
|
+
&& (!latest.expires_at || latest.expires_at >= latestValidAfter)) {
|
|
1015
|
+
process.stderr.write(`[openai-oauth] Reloaded tokens from disk\n`);
|
|
1016
|
+
return latest;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
if (!latest?.refresh_token) {
|
|
1020
|
+
if (!force && latest?.access_token && (!latest.expires_at || latest.expires_at > Date.now())) {
|
|
1021
|
+
process.stderr.write(`[openai-oauth] WARNING: token expiring but no refresh token; using current token until expiry\n`);
|
|
1022
|
+
this._refreshFallbackUntil = Date.now() + TOKEN_REFRESH_SKEW_MS;
|
|
1023
|
+
return latest;
|
|
1024
|
+
}
|
|
1025
|
+
throw new Error('OpenAI OAuth refresh token not available. Run codex login to re-authenticate.');
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
try {
|
|
1029
|
+
const _refreshT0 = Date.now();
|
|
1030
|
+
const _expiringInMs = (latest?.expires_at ?? 0) - Date.now();
|
|
1031
|
+
if (process.env.MIXDOG_DEBUG_BRIDGE) { process.stderr.write(`[bridge-trace] auth-refresh-needed expiringInMs=${_expiringInMs}\n`); }
|
|
1032
|
+
process.stderr.write(`[openai-oauth] Token ${reason}, refreshing...\n`);
|
|
1033
|
+
let refreshed;
|
|
1034
|
+
try {
|
|
1035
|
+
refreshed = await refreshTokens(latest.refresh_token);
|
|
1036
|
+
} catch (refreshErr) {
|
|
1037
|
+
// invalid_grant: the Codex CLI rotated this single-use refresh
|
|
1038
|
+
// token between our disk read and this refresh. Re-read both
|
|
1039
|
+
// stores and retry ONCE with the freshest different token.
|
|
1040
|
+
if (!refreshErr?.isInvalidGrant) throw refreshErr;
|
|
1041
|
+
process.stderr.write('[openai-oauth] invalid_grant — re-reading disk, retrying refresh\n');
|
|
1042
|
+
const candidates = [_loadOwnCodexTokens(), _loadCodexCliTokens()].filter(Boolean)
|
|
1043
|
+
.sort((a, b) => (b._mtimeMs || 0) - (a._mtimeMs || 0));
|
|
1044
|
+
const freshTok = candidates.find(c => c.refresh_token && c.refresh_token !== latest.refresh_token);
|
|
1045
|
+
if (!freshTok) throw refreshErr;
|
|
1046
|
+
refreshed = await refreshTokens(freshTok.refresh_token);
|
|
1047
|
+
}
|
|
1048
|
+
if (process.env.MIXDOG_DEBUG_BRIDGE) { process.stderr.write(`[bridge-trace] auth-refresh-done elapsed=${Date.now() - _refreshT0}ms ok=${!!refreshed}\n`); }
|
|
1049
|
+
if (!refreshed) throw new Error('refresh returned null');
|
|
1050
|
+
process.stderr.write(`[openai-oauth] Token refreshed, expires in ${Math.round(((refreshed.expires_at || Date.now()) - Date.now()) / 1000)}s\n`);
|
|
1051
|
+
return refreshed;
|
|
1052
|
+
}
|
|
1053
|
+
catch (err) {
|
|
1054
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1055
|
+
if (!force && latest?.access_token && (!latest.expires_at || latest.expires_at > Date.now())) {
|
|
1056
|
+
this._refreshFallbackUntil = Date.now() + TOKEN_REFRESH_SKEW_MS;
|
|
1057
|
+
process.stderr.write(`[openai-oauth] Refresh failed (${msg}); using still-valid current token\n`);
|
|
1058
|
+
return latest;
|
|
1059
|
+
}
|
|
1060
|
+
throw new Error(`OpenAI OAuth token refresh failed (${msg}). Run codex login to re-authenticate.`);
|
|
1061
|
+
}
|
|
1062
|
+
})().finally(() => { _oauthRefreshInFlight = null; });
|
|
1063
|
+
|
|
1064
|
+
this.tokens = await _oauthRefreshInFlight;
|
|
1065
|
+
return this.tokens;
|
|
1066
|
+
}
|
|
1067
|
+
async send(messages, model, tools, sendOpts) {
|
|
1068
|
+
const opts = sendOpts || {};
|
|
1069
|
+
const onStageChange = typeof opts.onStageChange === 'function' ? opts.onStageChange : null;
|
|
1070
|
+
const onStreamDelta = typeof opts.onStreamDelta === 'function' ? opts.onStreamDelta : null;
|
|
1071
|
+
const onToolCall = typeof opts.onToolCall === 'function' ? opts.onToolCall : null;
|
|
1072
|
+
const externalSignal = opts.signal || null;
|
|
1073
|
+
const _sendSessionId = opts.sessionId || '(none)';
|
|
1074
|
+
const _sendRole = opts.role || '(none)';
|
|
1075
|
+
if (process.env.MIXDOG_DEBUG_BRIDGE) { process.stderr.write(`[bridge-trace] auth-start sessionHash=${createHash('sha256').update(String(_sendSessionId)).digest('hex').slice(0, 8)} role=${_sendRole} expiringInMs=${this.tokens?.expires_at ? this.tokens.expires_at - Date.now() : 'unknown'}\n`); }
|
|
1076
|
+
// Build request body in parallel with auth resolution. ensureAuth is
|
|
1077
|
+
// a no-op fast-path on cached tokens, but a refresh round-trip can
|
|
1078
|
+
// take 300ms+; the body build (message serialisation) overlaps cleanly.
|
|
1079
|
+
const useModel = model || await ensureLatestCodexModel(this);
|
|
1080
|
+
// Escape hatch for callers (e.g. the web-search backend) that ship a
|
|
1081
|
+
// fully-formed request body with a server-side tool shape buildRequestBody
|
|
1082
|
+
// can't express. Routing through send() still gives them the 401/403
|
|
1083
|
+
// force-refresh retry + HTTP/SSE fallback instead of a hard fail.
|
|
1084
|
+
const _bodyP = opts._prebuiltBody
|
|
1085
|
+
? Promise.resolve(opts._prebuiltBody)
|
|
1086
|
+
: Promise.resolve().then(() => buildRequestBody(messages, useModel, tools, sendOpts));
|
|
1087
|
+
const _authP = this.ensureAuth();
|
|
1088
|
+
let auth = await _authP;
|
|
1089
|
+
const body = await _bodyP;
|
|
1090
|
+
// poolKey ≠ cacheKey by design (see openai-oauth-ws.mjs:57-68).
|
|
1091
|
+
// poolKey is per-session so parallel reviewer/worker callers each
|
|
1092
|
+
// get their own socket bucket — a sibling cannot grab a mid-turn
|
|
1093
|
+
// entry and trip Codex's "No tool call found for function call
|
|
1094
|
+
// output with call_id …" rejection. cacheKey is provider-scoped
|
|
1095
|
+
// (e.g. `mixdog-codex`) and feeds both `body.prompt_cache_key` and
|
|
1096
|
+
// the handshake `session_id` header, so all orchestrator-internal
|
|
1097
|
+
// dispatches land on the same server-side prompt-cache shard
|
|
1098
|
+
// regardless of which logical session opened the socket.
|
|
1099
|
+
// poolKey defaults to sessionId (per-session socket isolation); cacheKey
|
|
1100
|
+
// resolves to the shared 'mixdog-codex' shard (never sessionId) so a
|
|
1101
|
+
// fresh session reuses the warm prefix cache.
|
|
1102
|
+
const poolKey = opts.sessionId || null;
|
|
1103
|
+
const cacheKey = resolveProviderCacheKey(opts, 'openai-oauth');
|
|
1104
|
+
const iteration = Number.isFinite(Number(opts.iteration)) ? Number(opts.iteration) : null;
|
|
1105
|
+
const sendWs = typeof opts._sendViaWebSocketFn === 'function' ? opts._sendViaWebSocketFn : sendViaWebSocket;
|
|
1106
|
+
const sendHttp = typeof opts._sendViaHttpSseFn === 'function' ? opts._sendViaHttpSseFn : sendViaHttpSse;
|
|
1107
|
+
const _t1 = Date.now();
|
|
1108
|
+
const recordLiveModel = (result) => {
|
|
1109
|
+
if (result?.model && !_codexCatalogHas(result.model)) {
|
|
1110
|
+
void this._refreshModelCache();
|
|
1111
|
+
}
|
|
1112
|
+
return result;
|
|
1113
|
+
};
|
|
1114
|
+
const dispatchHttp = async (reason, originalErr = null) => {
|
|
1115
|
+
appendBridgeTrace({
|
|
1116
|
+
sessionId: poolKey,
|
|
1117
|
+
iteration,
|
|
1118
|
+
kind: 'transport_fallback',
|
|
1119
|
+
provider: 'openai-oauth',
|
|
1120
|
+
model: useModel,
|
|
1121
|
+
transport: 'http',
|
|
1122
|
+
payload: {
|
|
1123
|
+
from: 'websocket',
|
|
1124
|
+
to: 'http',
|
|
1125
|
+
reason,
|
|
1126
|
+
error_code: originalErr?.code || null,
|
|
1127
|
+
error_http_status: Number(originalErr?.httpStatus || 0) || null,
|
|
1128
|
+
error_classifier: originalErr?.retryClassifier || originalErr?.midstreamClassifier || null,
|
|
1129
|
+
},
|
|
1130
|
+
});
|
|
1131
|
+
process.stderr.write(`[openai-oauth] WebSocket unhealthy (${reason}); falling back to HTTP/SSE\n`);
|
|
1132
|
+
const result = await sendHttp({
|
|
1133
|
+
auth,
|
|
1134
|
+
body,
|
|
1135
|
+
opts,
|
|
1136
|
+
onStreamDelta,
|
|
1137
|
+
onToolCall,
|
|
1138
|
+
onStageChange,
|
|
1139
|
+
externalSignal,
|
|
1140
|
+
poolKey,
|
|
1141
|
+
cacheKey,
|
|
1142
|
+
iteration,
|
|
1143
|
+
useModel,
|
|
1144
|
+
fetchFn: opts._fetchFn,
|
|
1145
|
+
});
|
|
1146
|
+
this._forceHttpFallback = true;
|
|
1147
|
+
if (process.env.MIXDOG_DEBUG_BRIDGE) {
|
|
1148
|
+
process.stderr.write(`[bridge-trace] provider-send-end elapsed=${Date.now() - _t1}ms result=ok transport=http-fallback\n`);
|
|
1149
|
+
}
|
|
1150
|
+
return recordLiveModel(result);
|
|
1151
|
+
};
|
|
1152
|
+
const dispatchWs = (forceFresh = false) => sendWs({
|
|
1153
|
+
auth,
|
|
1154
|
+
body,
|
|
1155
|
+
sendOpts: opts,
|
|
1156
|
+
onStreamDelta,
|
|
1157
|
+
onToolCall,
|
|
1158
|
+
onStageChange,
|
|
1159
|
+
externalSignal,
|
|
1160
|
+
poolKey,
|
|
1161
|
+
cacheKey,
|
|
1162
|
+
iteration,
|
|
1163
|
+
useModel,
|
|
1164
|
+
displayModel: _displayCodexModel,
|
|
1165
|
+
forceFresh,
|
|
1166
|
+
});
|
|
1167
|
+
if (opts.forceHttpFallback === true
|
|
1168
|
+
|| this._forceHttpFallback
|
|
1169
|
+
|| _envFlag('MIXDOG_OPENAI_OAUTH_FORCE_HTTP_FALLBACK', false)) {
|
|
1170
|
+
return dispatchHttp('forced');
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// Prefer WebSocket for hot cache/delta transport; fall back to HTTP/SSE
|
|
1174
|
+
// after retry-exhausted handshake/acquire/no-first-event failures.
|
|
1175
|
+
try {
|
|
1176
|
+
if (process.env.MIXDOG_DEBUG_BRIDGE) { process.stderr.write(`[bridge-trace] provider-send-start model=${useModel} role=${_sendRole} sessionHash=${createHash('sha256').update(String(_sendSessionId)).digest('hex').slice(0, 8)} iteration=${iteration ?? '(none)'}\n`); }
|
|
1177
|
+
const result = await dispatchWs(false);
|
|
1178
|
+
if (process.env.MIXDOG_DEBUG_BRIDGE) { process.stderr.write(`[bridge-trace] provider-send-end elapsed=${Date.now() - _t1}ms result=ok\n`); }
|
|
1179
|
+
return recordLiveModel(result);
|
|
1180
|
+
} catch (err) {
|
|
1181
|
+
const status = err?.httpStatus;
|
|
1182
|
+
if (status === 401 || status === 403) {
|
|
1183
|
+
process.stderr.write(`[openai-oauth-ws] ${status} — forcing refresh and retrying once over WS\n`);
|
|
1184
|
+
if (process.env.MIXDOG_DEBUG_BRIDGE) { process.stderr.write(`[bridge-trace] provider-${status}-retry attempt=1\n`); }
|
|
1185
|
+
this._refreshFallbackUntil = 0;
|
|
1186
|
+
auth = await this.ensureAuth({ forceRefresh: true, reason: String(status) });
|
|
1187
|
+
try {
|
|
1188
|
+
const result = await dispatchWs(true);
|
|
1189
|
+
if (process.env.MIXDOG_DEBUG_BRIDGE) { process.stderr.write(`[bridge-trace] provider-send-end elapsed=${Date.now() - _t1}ms result=ok\n`); }
|
|
1190
|
+
return recordLiveModel(result);
|
|
1191
|
+
} catch (retryErr) {
|
|
1192
|
+
if (_shouldUseOpenAIHttpFallback(retryErr, externalSignal)) {
|
|
1193
|
+
try {
|
|
1194
|
+
return await dispatchHttp(retryErr?.retryClassifier || retryErr?.code || retryErr?.message || 'ws_auth_retry_failed', retryErr);
|
|
1195
|
+
} catch (fallbackErr) {
|
|
1196
|
+
try { retryErr.fallbackError = fallbackErr; } catch {}
|
|
1197
|
+
throw retryErr;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
throw retryErr;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
const msg = err?.message || '';
|
|
1204
|
+
const isUnknownModel = status === 404
|
|
1205
|
+
|| /unknown[_\s-]?model|model[_\s-]?not[_\s-]?found/i.test(msg);
|
|
1206
|
+
if (isUnknownModel && !opts._modelRetry) {
|
|
1207
|
+
process.stderr.write(`[openai-oauth-ws] unknown model — refreshing catalog + 1 retry\n`);
|
|
1208
|
+
await this._refreshModelCache();
|
|
1209
|
+
return this.send(messages, model, tools, { ...opts, _modelRetry: true });
|
|
1210
|
+
}
|
|
1211
|
+
if (_shouldUseOpenAIHttpFallback(err, externalSignal)) {
|
|
1212
|
+
try {
|
|
1213
|
+
return await dispatchHttp(err?.retryClassifier || err?.midstreamClassifier || err?.code || err?.message || 'ws_failed', err);
|
|
1214
|
+
} catch (fallbackErr) {
|
|
1215
|
+
try { err.fallbackError = fallbackErr; } catch {}
|
|
1216
|
+
throw err;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
throw err;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
async listModels() {
|
|
1223
|
+
// Dynamic lookup via Codex /backend-api/codex/models. Cached 24h.
|
|
1224
|
+
// Endpoint returns rich metadata (context_window, reasoning levels,
|
|
1225
|
+
// visibility) that is more detailed than /v1/models.
|
|
1226
|
+
const cached = await _loadCodexModelCache();
|
|
1227
|
+
if (cached) {
|
|
1228
|
+
_lastCodexListModelsError = '';
|
|
1229
|
+
_inMemoryCodexCatalog = cached.slice();
|
|
1230
|
+
return cached;
|
|
1231
|
+
}
|
|
1232
|
+
try {
|
|
1233
|
+
const auth = await this.ensureAuth();
|
|
1234
|
+
const clientVersion = await _resolveCodexClientVersion();
|
|
1235
|
+
const url = `https://chatgpt.com/backend-api/codex/models?client_version=${clientVersion}`;
|
|
1236
|
+
const res = await fetch(url, {
|
|
1237
|
+
signal: AbortSignal.timeout(10_000),
|
|
1238
|
+
method: 'GET',
|
|
1239
|
+
headers: {
|
|
1240
|
+
'Authorization': `Bearer ${auth.access_token}`,
|
|
1241
|
+
'OpenAI-Beta': 'responses=experimental',
|
|
1242
|
+
'originator': 'codex_cli_rs',
|
|
1243
|
+
'chatgpt-account-id': auth.account_id || '',
|
|
1244
|
+
},
|
|
1245
|
+
dispatcher: getLlmDispatcher(),
|
|
1246
|
+
});
|
|
1247
|
+
if (!res.ok) throw new Error(`codex list_models ${res.status}`);
|
|
1248
|
+
const data = await res.json();
|
|
1249
|
+
const items = Array.isArray(data?.models) ? data.models : [];
|
|
1250
|
+
const normalized = items.map(m => _normalizeCodexModel(m));
|
|
1251
|
+
_markLatestCodex(normalized);
|
|
1252
|
+
const enriched = await enrichModels(normalized);
|
|
1253
|
+
await _saveCodexModelCache(enriched);
|
|
1254
|
+
_lastCodexListModelsError = '';
|
|
1255
|
+
return enriched;
|
|
1256
|
+
} catch (err) {
|
|
1257
|
+
_lastCodexListModelsError = err?.message || String(err);
|
|
1258
|
+
process.stderr.write(`[openai-oauth] listModels fetch failed (${_lastCodexListModelsError})\n`);
|
|
1259
|
+
// No fallback catalog — empty list signals the UI to show a
|
|
1260
|
+
// "catalog unavailable, retry" state. Codex has no equivalent to
|
|
1261
|
+
// Anthropic's family tokens so there's no meaningful minimal list.
|
|
1262
|
+
return [];
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
// Force a catalog refresh (ignores 24h TTL). De-duped via
|
|
1266
|
+
// _codexRefreshInFlight so concurrent callers share one HTTP round-trip.
|
|
1267
|
+
async _refreshModelCache() {
|
|
1268
|
+
if (_codexRefreshInFlight) return _codexRefreshInFlight;
|
|
1269
|
+
_codexRefreshInFlight = (async () => {
|
|
1270
|
+
try {
|
|
1271
|
+
const auth = await this.ensureAuth();
|
|
1272
|
+
const clientVersion = await _resolveCodexClientVersion();
|
|
1273
|
+
const url = `https://chatgpt.com/backend-api/codex/models?client_version=${clientVersion}`;
|
|
1274
|
+
const res = await fetch(url, {
|
|
1275
|
+
signal: AbortSignal.timeout(10_000),
|
|
1276
|
+
method: 'GET',
|
|
1277
|
+
headers: {
|
|
1278
|
+
'Authorization': `Bearer ${auth.access_token}`,
|
|
1279
|
+
'OpenAI-Beta': 'responses=experimental',
|
|
1280
|
+
'originator': 'codex_cli_rs',
|
|
1281
|
+
'chatgpt-account-id': auth.account_id || '',
|
|
1282
|
+
},
|
|
1283
|
+
dispatcher: getLlmDispatcher(),
|
|
1284
|
+
});
|
|
1285
|
+
if (!res.ok) throw new Error(`codex list_models ${res.status}`);
|
|
1286
|
+
const data = await res.json();
|
|
1287
|
+
const items = Array.isArray(data?.models) ? data.models : [];
|
|
1288
|
+
const normalized = items.map(m => _normalizeCodexModel(m));
|
|
1289
|
+
_markLatestCodex(normalized);
|
|
1290
|
+
const enriched = await enrichModels(normalized);
|
|
1291
|
+
await _saveCodexModelCache(enriched);
|
|
1292
|
+
process.stderr.write(`[openai-oauth] catalog refreshed (${enriched.length} models)\n`);
|
|
1293
|
+
return enriched;
|
|
1294
|
+
} catch (err) {
|
|
1295
|
+
process.stderr.write(`[openai-oauth] catalog refresh failed (${err.message})\n`);
|
|
1296
|
+
return null;
|
|
1297
|
+
} finally {
|
|
1298
|
+
_codexRefreshInFlight = null;
|
|
1299
|
+
}
|
|
1300
|
+
})();
|
|
1301
|
+
return _codexRefreshInFlight;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
async isAvailable() {
|
|
1305
|
+
return this.tokens !== null;
|
|
1306
|
+
}
|
|
1307
|
+
}
|