mixdog 0.9.2 → 0.9.4
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/package.json +8 -3
- package/scripts/anthropic-maxtokens-test.mjs +119 -0
- package/scripts/bench/lead-review-tasks-r3.json +20 -0
- package/scripts/bench/lead-review-tasks.json +20 -0
- package/scripts/bench/r4-mixed-tasks.json +20 -0
- package/scripts/bench/review-tasks.json +20 -0
- package/scripts/bench/round-codex.json +114 -0
- package/scripts/bench/round-mixdog-lead-r3.json +269 -0
- package/scripts/bench/round-mixdog-lead.json +269 -0
- package/scripts/bench/round-mixdog.json +126 -0
- package/scripts/bench/round-r10-bigsample.json +679 -0
- package/scripts/bench/round-r11-codexalign.json +257 -0
- package/scripts/bench/round-r4-codex.json +114 -0
- package/scripts/bench/round-r4-mixed.json +225 -0
- package/scripts/bench/round-r5-gpt-lead.json +259 -0
- package/scripts/bench/round-r6-codex.json +114 -0
- package/scripts/bench/round-r6-solo.json +257 -0
- package/scripts/bench/round-r7-full.json +254 -0
- package/scripts/bench/round-r8-fulldefault.json +255 -0
- package/scripts/bench-run.mjs +215 -29
- package/scripts/build-tui.mjs +13 -1
- package/scripts/explore-bench.mjs +124 -0
- package/scripts/freevar-smoke.mjs +95 -0
- package/scripts/hook-bus-test.mjs +191 -0
- package/scripts/internal-comms-bench.mjs +1 -0
- package/scripts/internal-comms-smoke.mjs +10 -9
- package/scripts/mouse-probe.mjs +45 -0
- package/scripts/output-style-bench.mjs +13 -6
- package/scripts/output-style-smoke.mjs +4 -4
- package/scripts/path-suffix-test.mjs +57 -0
- package/scripts/provider-toolcall-test.mjs +7 -3
- package/scripts/recall-bench.mjs +207 -0
- package/scripts/recall-usecase-cases.json +18 -0
- package/scripts/recall-usecase-probe.json +6 -0
- package/scripts/session-bench.mjs +152 -6
- package/scripts/tool-smoke.mjs +30 -67
- package/scripts/tui-render-smoke.mjs +90 -0
- package/scripts/webhook-smoke.mjs +208 -0
- package/src/agents/debugger/AGENT.md +5 -2
- package/src/agents/heavy-worker/AGENT.md +21 -11
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +3 -2
- package/src/agents/worker/AGENT.md +21 -11
- package/src/lib/rules-builder.cjs +4 -0
- package/src/mixdog-session-runtime.mjs +933 -3731
- package/src/output-styles/default.md +34 -9
- package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
- package/src/output-styles/minimal.md +4 -1
- package/src/output-styles/simple.md +22 -7
- package/src/repl.mjs +5 -5
- package/src/rules/agent/00-common.md +2 -0
- package/src/rules/agent/30-explorer.md +8 -11
- package/src/rules/lead/lead-brief.md +12 -0
- package/src/rules/lead/lead-tool.md +2 -9
- package/src/rules/shared/01-tool.md +11 -5
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
- package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
- package/src/runtime/agent/orchestrator/config.mjs +69 -2
- package/src/runtime/agent/orchestrator/context/collect.mjs +51 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +63 -21
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +97 -1343
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +78 -10
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +1 -13
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +44 -1014
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +18 -4
- package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +86 -11
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +348 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +19 -12
- package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +41 -1142
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +732 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +303 -2119
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +140 -995
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +227 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +436 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +38 -12
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +7 -8
- package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
- package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
- package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
- package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
- package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +274 -0
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +47 -0
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +182 -0
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +409 -1304
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +230 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +149 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +226 -2114
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
- package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +33 -42
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -87
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +78 -304
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -6
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +551 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1080 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +36 -4277
- package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
- package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
- package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -23
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +10 -74
- package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
- package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
- package/src/runtime/channels/backends/discord-access.mjs +32 -0
- package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
- package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
- package/src/runtime/channels/backends/discord.mjs +12 -292
- package/src/runtime/channels/index.mjs +241 -894
- package/src/runtime/channels/lib/backend-dispatch.mjs +44 -0
- package/src/runtime/channels/lib/boot-profile.mjs +23 -0
- package/src/runtime/channels/lib/crash-log.mjs +106 -0
- package/src/runtime/channels/lib/event-pipeline.mjs +18 -1
- package/src/runtime/channels/lib/event-queue.mjs +63 -4
- package/src/runtime/channels/lib/inbound-routing.mjs +111 -0
- package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +9 -1
- package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
- package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
- package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
- package/src/runtime/channels/lib/session-discovery.mjs +56 -4
- package/src/runtime/channels/lib/telegram-format.mjs +19 -22
- package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
- package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
- package/src/runtime/channels/lib/webhook/deliveries.mjs +312 -0
- package/src/runtime/channels/lib/webhook/log.mjs +42 -0
- package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
- package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
- package/src/runtime/channels/lib/webhook.mjs +36 -570
- package/src/runtime/channels/lib/whisper-language.mjs +42 -0
- package/src/runtime/channels/tool-defs.mjs +11 -130
- package/src/runtime/memory/index.mjs +258 -2050
- package/src/runtime/memory/lib/core-memory-store.mjs +351 -1
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
- package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
- package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
- package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
- package/src/runtime/memory/lib/http-wire.mjs +57 -0
- package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
- package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +72 -837
- package/src/runtime/memory/lib/memory-embed.mjs +149 -0
- package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +22 -2
- package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
- package/src/runtime/memory/lib/memory.mjs +20 -0
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
- package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
- package/src/runtime/memory/lib/query-handlers.mjs +780 -0
- package/src/runtime/memory/lib/recall-format.mjs +238 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
- package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
- package/src/runtime/memory/tool-defs.mjs +6 -14
- package/src/runtime/search/lib/http-fetch.mjs +274 -0
- package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
- package/src/runtime/search/lib/web-tools.mjs +24 -602
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +26 -1
- package/src/runtime/shared/background-tasks.mjs +2 -3
- package/src/runtime/shared/buffered-appender.mjs +149 -0
- package/src/runtime/shared/launcher-control.mjs +2 -2
- package/src/runtime/shared/task-notification-envelope.mjs +98 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/tool-primitives.mjs +308 -0
- package/src/runtime/shared/tool-result-summary.mjs +515 -0
- package/src/runtime/shared/tool-surface.mjs +80 -898
- package/src/runtime/shared/transcript-writer.mjs +52 -2
- package/src/runtime/shared/update-checker.mjs +7 -4
- package/src/session-runtime/config-helpers.mjs +291 -0
- package/src/session-runtime/config-lifecycle.mjs +232 -0
- package/src/session-runtime/cwd-plugins.mjs +226 -0
- package/src/session-runtime/effort.mjs +128 -0
- package/src/session-runtime/fs-utils.mjs +10 -0
- package/src/session-runtime/mcp-glue.mjs +177 -0
- package/src/session-runtime/model-capabilities.mjs +130 -0
- package/src/session-runtime/model-recency.mjs +111 -0
- package/src/session-runtime/native-search.mjs +247 -0
- package/src/session-runtime/output-styles.mjs +126 -0
- package/src/session-runtime/plugin-mcp.mjs +114 -0
- package/src/session-runtime/prewarm.mjs +142 -0
- package/src/session-runtime/provider-models.mjs +278 -0
- package/src/session-runtime/provider-usage.mjs +120 -0
- package/src/session-runtime/quick-model-rows.mjs +170 -0
- package/src/session-runtime/quick-search-models.mjs +46 -0
- package/src/session-runtime/session-hooks.mjs +93 -0
- package/src/session-runtime/session-text.mjs +100 -0
- package/src/session-runtime/settings-api.mjs +319 -0
- package/src/session-runtime/statusline-route.mjs +35 -0
- package/src/session-runtime/tool-catalog.mjs +720 -0
- package/src/session-runtime/tool-defs.mjs +84 -0
- package/src/session-runtime/warmup-schedulers.mjs +201 -0
- package/src/session-runtime/workflow.mjs +358 -0
- package/src/standalone/agent-tool/helpers.mjs +237 -0
- package/src/standalone/agent-tool/notify.mjs +107 -0
- package/src/standalone/agent-tool/provider-init.mjs +143 -0
- package/src/standalone/agent-tool/render.mjs +152 -0
- package/src/standalone/agent-tool/tool-def.mjs +55 -0
- package/src/standalone/agent-tool.mjs +155 -677
- package/src/standalone/channel-worker.mjs +7 -9
- package/src/standalone/explore-tool.mjs +40 -12
- package/src/standalone/hook-bus/config.mjs +207 -0
- package/src/standalone/hook-bus/constants.mjs +90 -0
- package/src/standalone/hook-bus/handlers.mjs +481 -0
- package/src/standalone/hook-bus/payload.mjs +31 -0
- package/src/standalone/hook-bus/rules.mjs +77 -0
- package/src/standalone/hook-bus.mjs +110 -746
- package/src/standalone/memory-runtime-proxy.mjs +7 -0
- package/src/standalone/opencode-go-login.mjs +125 -0
- package/src/standalone/provider-admin.mjs +15 -19
- package/src/standalone/usage-dashboard.mjs +3 -1
- package/src/tui/App.jsx +1163 -7571
- package/src/tui/app/app-format.mjs +206 -0
- package/src/tui/app/channel-pickers.mjs +510 -0
- package/src/tui/app/clipboard.mjs +67 -0
- package/src/tui/app/core-memory-picker.mjs +210 -0
- package/src/tui/app/extension-pickers.mjs +506 -0
- package/src/tui/app/input-parsers.mjs +193 -0
- package/src/tui/app/maintenance-pickers.mjs +324 -0
- package/src/tui/app/model-options.mjs +330 -0
- package/src/tui/app/model-picker.mjs +365 -0
- package/src/tui/app/onboarding-steps.mjs +400 -0
- package/src/tui/app/project-picker.mjs +247 -0
- package/src/tui/app/provider-setup-picker.mjs +580 -0
- package/src/tui/app/resume-picker.mjs +55 -0
- package/src/tui/app/route-pickers.mjs +419 -0
- package/src/tui/app/settings-picker.mjs +490 -0
- package/src/tui/app/slash-commands.mjs +101 -0
- package/src/tui/app/slash-dispatch.mjs +427 -0
- package/src/tui/app/text-layout.mjs +46 -0
- package/src/tui/app/theme-effort-pickers.mjs +154 -0
- package/src/tui/app/transcript-window.mjs +671 -0
- package/src/tui/app/use-mouse-input.mjs +460 -0
- package/src/tui/app/use-prompt-handlers.mjs +310 -0
- package/src/tui/app/use-transcript-scroll.mjs +510 -0
- package/src/tui/app/use-transcript-window.mjs +589 -0
- package/src/tui/components/ConfirmBar.jsx +1 -1
- package/src/tui/components/Picker.jsx +32 -4
- package/src/tui/components/PromptInput.jsx +259 -80
- package/src/tui/components/SlashCommandPalette.jsx +8 -1
- package/src/tui/components/StatusLine.jsx +63 -12
- package/src/tui/components/TextEntryPanel.jsx +11 -0
- package/src/tui/components/ToolExecution.jsx +56 -588
- package/src/tui/components/TranscriptItem.jsx +105 -0
- package/src/tui/components/UsagePanel.jsx +18 -4
- package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
- package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
- package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
- package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
- package/src/tui/components/tool-execution/text-format.mjs +161 -0
- package/src/tui/components/tool-output-format.mjs +2 -2
- package/src/tui/display-width.mjs +20 -3
- package/src/tui/dist/index.mjs +18034 -17188
- package/src/tui/engine/agent-envelope.mjs +296 -0
- package/src/tui/engine/agent-job-feed.mjs +133 -0
- package/src/tui/engine/boot-profile.mjs +21 -0
- package/src/tui/engine/labels.mjs +67 -0
- package/src/tui/engine/notice-text.mjs +112 -0
- package/src/tui/engine/notification-plan.mjs +76 -0
- package/src/tui/engine/queue-helpers.mjs +161 -0
- package/src/tui/engine/render-timing.mjs +17 -0
- package/src/tui/engine/session-stats.mjs +46 -0
- package/src/tui/engine/tool-approval.mjs +94 -0
- package/src/tui/engine/tool-call-fields.mjs +23 -0
- package/src/tui/engine/tool-card-results.mjs +234 -0
- package/src/tui/engine/tool-result-status.mjs +135 -0
- package/src/tui/engine/tool-result-text.mjs +126 -0
- package/src/tui/engine.mjs +405 -1385
- package/src/tui/figures.mjs +5 -0
- package/src/tui/index.jsx +105 -0
- package/src/tui/input-editing.mjs +60 -10
- package/src/tui/keyboard-protocol.mjs +2 -2
- package/src/tui/lib/voice-recorder.mjs +35 -19
- package/src/tui/markdown/format-token.mjs +11 -9
- package/src/tui/paste-attachments.mjs +38 -0
- package/src/tui/statusline-ansi-bridge.mjs +11 -3
- package/src/tui/theme.mjs +6 -0
- package/src/tui/themes/base.mjs +2 -2
- package/src/tui/themes/kanagawa.mjs +4 -4
- package/src/tui/themes/teal.mjs +4 -5
- package/src/tui/themes/utils.mjs +1 -1
- package/src/ui/statusline-agents.mjs +213 -0
- package/src/ui/statusline-format.mjs +146 -0
- package/src/ui/statusline-segments.mjs +148 -0
- package/src/ui/statusline.mjs +77 -462
- package/src/ui/tool-card.mjs +0 -1
- package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
- package/src/workflows/default/WORKFLOW.md +16 -9
- package/src/workflows/sequential/WORKFLOW.md +16 -11
- package/src/workflows/solo/WORKFLOW.md +5 -1
- package/vendor/ink/build/display-width.js +19 -3
- package/vendor/ink/build/ink.js +103 -6
- package/vendor/ink/build/log-update.js +17 -3
- package/vendor/ink/build/wrap-text.js +125 -0
- package/scripts/_test-folder-dialog.mjs +0 -30
- package/scripts/fix-brief-fn.mjs +0 -35
- package/scripts/fix-format-tool-surface.mjs +0 -24
- package/scripts/fix-tool-exec-visible.mjs +0 -42
- package/scripts/patch-agent-brief.mjs +0 -48
- package/scripts/patch-app.mjs +0 -21
- package/scripts/patch-app2.mjs +0 -18
- package/scripts/patch-dist-brief.mjs +0 -96
- package/scripts/patch-tool-exec.mjs +0 -70
- package/src/examples/schedules/SCHEDULE.example.md +0 -32
- package/src/examples/webhooks/WEBHOOK.example.md +0 -40
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
- package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
- package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
- package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
- package/src/standalone/agent-task-status.test.mjs +0 -76
- package/src/tui/components/tool-output-format.test.mjs +0 -399
- package/src/tui/display-width.test.mjs +0 -35
- package/src/tui/engine-runtime-notification.test.mjs +0 -115
- package/src/tui/engine-tool-result-text.test.mjs +0 -75
- package/src/tui/markdown/format-token.test.mjs +0 -354
- package/src/tui/markdown/render-ansi.test.mjs +0 -108
- package/src/tui/markdown/stream-fence.test.mjs +0 -26
- package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
- package/src/tui/prompt-history-store.test.mjs +0 -52
- package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
- package/src/tui/transcript-tool-failures.test.mjs +0 -111
- package/src/ui/markdown.test.mjs +0 -70
- package/src/ui/statusline-context-label.test.mjs +0 -15
- package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
- package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
* openai-oauth-ws.mjs; this file owns auth, model catalog, request-body
|
|
8
8
|
* shape, and HTTP/SSE fallback when WebSocket transport is unhealthy.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
11
|
-
import { randomBytes, createHash } from 'crypto';
|
|
10
|
+
import { createHash } from 'crypto';
|
|
12
11
|
import { readFileSync, existsSync, mkdirSync, statSync, unlinkSync } from 'fs';
|
|
13
12
|
import { join } from 'path';
|
|
14
13
|
import { getPluginData } from '../config.mjs';
|
|
15
14
|
import { enrichModels } from './model-catalog.mjs';
|
|
15
|
+
import { sanitizeModelList } from './model-list-sanitize.mjs';
|
|
16
16
|
import { writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
|
|
17
17
|
import { makeModelCache } from './model-cache.mjs';
|
|
18
18
|
|
|
@@ -51,20 +51,43 @@ import {
|
|
|
51
51
|
isResponsesFreeformTool,
|
|
52
52
|
toResponsesCustomTool,
|
|
53
53
|
} from './custom-tool-wire.mjs';
|
|
54
|
+
import {
|
|
55
|
+
sendViaHttpSse,
|
|
56
|
+
_envFlag,
|
|
57
|
+
_envPositiveInt,
|
|
58
|
+
_shouldUseOpenAIHttpFallback,
|
|
59
|
+
} from './openai-oauth-http-sse.mjs';
|
|
60
|
+
import { createOpenAIOAuthLogin } from './openai-oauth-login.mjs';
|
|
61
|
+
import {
|
|
62
|
+
_displayCodexModel,
|
|
63
|
+
_codexFamily,
|
|
64
|
+
_normalizeCodexModel,
|
|
65
|
+
_compareVersion,
|
|
66
|
+
_isMainCodexFamily,
|
|
67
|
+
_markLatestCodex,
|
|
68
|
+
} from './openai-codex-model.mjs';
|
|
69
|
+
export { _displayCodexModel };
|
|
70
|
+
|
|
71
|
+
// Legacy import path for scripts/tool-smoke.mjs (single-emit SSE smoke).
|
|
72
|
+
export { sendViaHttpSse };
|
|
54
73
|
// --- Constants ---
|
|
55
74
|
const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
56
|
-
|
|
75
|
+
// Exported for openai-oauth-http-sse.mjs (fallback transport headers/URL).
|
|
76
|
+
export const CODEX_OAUTH_ORIGINATOR = 'codex_cli_rs';
|
|
57
77
|
const TOKEN_URL = 'https://auth.openai.com/oauth/token';
|
|
58
|
-
const CODEX_RESPONSES_URL = 'https://chatgpt.com/backend-api/codex/responses';
|
|
59
|
-
// Version string baked into the models endpoint query — the OAuth backend
|
|
60
|
-
// request without it
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
// on
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
|
|
78
|
+
export const CODEX_RESPONSES_URL = 'https://chatgpt.com/backend-api/codex/responses';
|
|
79
|
+
// Version string baked into the models endpoint query — the OAuth backend
|
|
80
|
+
// rejects the request without it, and gates new model exposures (e.g.
|
|
81
|
+
// gpt-5.5 only on >= 0.130.0) on this client_version header; older versions
|
|
82
|
+
// trigger a visibility-filtered catalog (e.g. only rollout models). Resolved
|
|
83
|
+
// dynamically from npm so newly-shipped models surface within a day instead
|
|
84
|
+
// of waiting on a hardcoded bump here. Cached 24h in-process; npm failure
|
|
85
|
+
// falls back to the floor below.
|
|
86
|
+
// Offline fallback only — _resolveCodexClientVersion() fetches the live
|
|
87
|
+
// @openai/codex latest from npm first. Bumped to the current release
|
|
88
|
+
// (0.142.5, verified 2026-07-03) so the offline path stays close to what the
|
|
89
|
+
// backend expects for client-version gating.
|
|
90
|
+
const CODEX_CLIENT_VERSION_FLOOR = '0.142.5';
|
|
68
91
|
const CODEX_VERSION_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
69
92
|
let _codexVersionCache = { value: null, fetchedAt: 0 };
|
|
70
93
|
|
|
@@ -119,10 +142,6 @@ let _codexRefreshInFlight = null;
|
|
|
119
142
|
let _oauthRefreshInFlight = null;
|
|
120
143
|
let _lastCodexListModelsError = '';
|
|
121
144
|
|
|
122
|
-
export function getOpenAIOAuthModelCatalogError() {
|
|
123
|
-
return _lastCodexListModelsError;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
145
|
function _codexCatalogHas(id) {
|
|
127
146
|
if (!id || !Array.isArray(_inMemoryCodexCatalog)) return false;
|
|
128
147
|
return _inMemoryCodexCatalog.some(m => m.id === id);
|
|
@@ -156,106 +175,6 @@ export function codexModelSupportsServiceTier(id, serviceTier) {
|
|
|
156
175
|
return tiers.some(t => t?.id === serviceTier);
|
|
157
176
|
}
|
|
158
177
|
|
|
159
|
-
// OAuth catalog returns dated ids (gpt-5.4-mini-2026-03-17). Strip the trailing
|
|
160
|
-
// -YYYY-MM-DD to get the version alias (gpt-5.4-mini). Unknown shapes pass
|
|
161
|
-
// through unchanged.
|
|
162
|
-
function _displayCodexModel(id) {
|
|
163
|
-
if (!id || typeof id !== 'string') return id;
|
|
164
|
-
return id.replace(/-\d{4}-\d{2}-\d{2}$/, '');
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function _positiveCodexContextWindow(value) {
|
|
168
|
-
const n = Number(value);
|
|
169
|
-
return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function _codexContextWindowFromApi(m) {
|
|
173
|
-
return _positiveCodexContextWindow(m?.context_window)
|
|
174
|
-
|| _positiveCodexContextWindow(m?.max_context_window)
|
|
175
|
-
|| null;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function _normalizeCodexModel(m) {
|
|
179
|
-
const id = m?.slug || m?.id;
|
|
180
|
-
const family = _codexFamily(id);
|
|
181
|
-
const serviceTiers = Array.isArray(m?.service_tiers)
|
|
182
|
-
? m.service_tiers
|
|
183
|
-
.map(t => ({
|
|
184
|
-
id: String(t?.id || '').trim(),
|
|
185
|
-
name: String(t?.name || '').trim(),
|
|
186
|
-
description: String(t?.description || '').trim(),
|
|
187
|
-
}))
|
|
188
|
-
.filter(t => t.id)
|
|
189
|
-
: [];
|
|
190
|
-
const additionalSpeedTiers = Array.isArray(m?.additional_speed_tiers)
|
|
191
|
-
? m.additional_speed_tiers.map(t => String(t || '').trim()).filter(Boolean)
|
|
192
|
-
: [];
|
|
193
|
-
// Catalog ids are version aliases without separate display dating.
|
|
194
|
-
return {
|
|
195
|
-
id,
|
|
196
|
-
name: m?.display_name || id,
|
|
197
|
-
display: m?.display_name || id,
|
|
198
|
-
family,
|
|
199
|
-
provider: 'openai-oauth',
|
|
200
|
-
contextWindow: _codexContextWindowFromApi(m),
|
|
201
|
-
maxContextWindow: _positiveCodexContextWindow(m?.max_context_window),
|
|
202
|
-
outputTokens: m?.max_output_tokens || m?.output_tokens || 32768,
|
|
203
|
-
autoCompactTokenLimit: m?.auto_compact_token_limit || null,
|
|
204
|
-
effectiveContextWindowPercent: m?.effective_context_window_percent || null,
|
|
205
|
-
tier: 'version',
|
|
206
|
-
latest: false,
|
|
207
|
-
description: m?.description || '',
|
|
208
|
-
reasoningLevels: (m?.supported_reasoning_levels || []).map(r => r.effort),
|
|
209
|
-
serviceTiers,
|
|
210
|
-
defaultServiceTier: m?.default_service_tier || null,
|
|
211
|
-
additionalSpeedTiers,
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function _codexFamily(id) {
|
|
216
|
-
const s = String(id || '').toLowerCase();
|
|
217
|
-
if (s.includes('nano')) return 'gpt-nano';
|
|
218
|
-
if (s.includes('mini')) return 'gpt-mini';
|
|
219
|
-
if (s.includes('codex')) return 'gpt-codex';
|
|
220
|
-
if (s.startsWith('gpt-5.5')) return 'gpt-5.5';
|
|
221
|
-
if (s.startsWith('gpt-5.4')) return 'gpt-5.4';
|
|
222
|
-
if (s.startsWith('gpt-5.2')) return 'gpt-5.2';
|
|
223
|
-
if (s.startsWith('gpt-5')) return 'gpt-5';
|
|
224
|
-
return 'gpt';
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Compare two model ids by the X.Y version embedded in `gpt-X.Y`. Mirrors
|
|
228
|
-
// anthropic-oauth's _compareVersion; these ids have no trailing date so
|
|
229
|
-
// the version lives in the dotted number, not a -YYYY-MM-DD suffix.
|
|
230
|
-
function _compareVersion(a, b) {
|
|
231
|
-
const na = (String(a).match(/gpt-(\d+)\.(\d+)/) || []).slice(1).map(Number);
|
|
232
|
-
const nb = (String(b).match(/gpt-(\d+)\.(\d+)/) || []).slice(1).map(Number);
|
|
233
|
-
for (let i = 0; i < Math.max(na.length, nb.length); i++) {
|
|
234
|
-
if ((na[i] || 0) !== (nb[i] || 0)) return (na[i] || 0) - (nb[i] || 0);
|
|
235
|
-
}
|
|
236
|
-
return String(a).localeCompare(String(b));
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Main gpt-5 chat family only: exclude the mini/nano/codex variants so "latest"
|
|
240
|
-
// resolves to the flagship, not a smaller sibling.
|
|
241
|
-
function _isMainCodexFamily(family) {
|
|
242
|
-
return typeof family === 'string' && family.startsWith('gpt-5');
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Mark the highest-version model per family as `latest: true`. VERSION-based
|
|
246
|
-
// (ids carry no `created`), mirroring anthropic-oauth's per-family pass.
|
|
247
|
-
function _markLatestCodex(models) {
|
|
248
|
-
const byFamily = new Map();
|
|
249
|
-
for (const m of models) {
|
|
250
|
-
if (!m?.id) continue;
|
|
251
|
-
const cur = byFamily.get(m.family);
|
|
252
|
-
if (!cur || _compareVersion(m.id, cur.id) > 0) {
|
|
253
|
-
byFamily.set(m.family, m);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
for (const m of byFamily.values()) m.latest = true;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
178
|
// Newest MAIN gpt-5 chat model by version, read from the SYNC in-memory
|
|
260
179
|
// catalog mirror. Returns null until populated; callers warm via
|
|
261
180
|
// ensureLatestCodexModel when null.
|
|
@@ -578,7 +497,35 @@ function toOpenAIResponsesTool(t) {
|
|
|
578
497
|
};
|
|
579
498
|
}
|
|
580
499
|
|
|
500
|
+
// codex build_reasoning() (core/src/client.rs:785-805) only attaches the
|
|
501
|
+
// reasoning object when model_info.supports_reasoning_summaries; models
|
|
502
|
+
// without summary support get NO reasoning field at all. Mirror that via the
|
|
503
|
+
// cached codex catalog; unknown models default to true (gpt-5 family all
|
|
504
|
+
// support summaries) so a cold catalog cannot strip reasoning from the wire.
|
|
505
|
+
function _codexModelSupportsReasoningSummaries(id) {
|
|
506
|
+
const info = _findCachedCodexModel(id);
|
|
507
|
+
if (!info) return true;
|
|
508
|
+
const flags = [info.supportsReasoningSummaries, info.supports_reasoning_summaries, info.supportsReasoning, info.supports_reasoning];
|
|
509
|
+
for (const flag of flags) {
|
|
510
|
+
if (typeof flag === 'boolean') return flag;
|
|
511
|
+
}
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// codex reasoning_effort_for_request (core/src/client.rs): `ultra` collapses to
|
|
516
|
+
// `max` on the wire — the openai-oauth backend does not accept `ultra`. Every
|
|
517
|
+
// other effort passes through unchanged; empty/unknown falls back to medium.
|
|
518
|
+
function _normalizeReasoningEffort(effort) {
|
|
519
|
+
const e = String(effort || '').trim().toLowerCase();
|
|
520
|
+
if (!e) return 'medium';
|
|
521
|
+
if (e === 'ultra') return 'max';
|
|
522
|
+
return e;
|
|
523
|
+
}
|
|
524
|
+
|
|
581
525
|
export function buildRequestBody(messages, model, tools, sendOpts) {
|
|
526
|
+
// codex reasoning_effort_for_request: `ultra` collapses to `max` on the
|
|
527
|
+
// wire (the only remap; every other effort passes through). Default medium.
|
|
528
|
+
// Kept inline (not a module const) so buildRequestBody stays self-contained.
|
|
582
529
|
// Extract system/instructions
|
|
583
530
|
const systemMsgs = messages.filter(m => m.role === 'system');
|
|
584
531
|
const instructions = systemMsgs.map(m => m.content).join('\n\n') || 'You are a helpful assistant.';
|
|
@@ -587,7 +534,7 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
587
534
|
providerState: opts.providerState,
|
|
588
535
|
model,
|
|
589
536
|
});
|
|
590
|
-
// Match the body shape
|
|
537
|
+
// Match the request body shape the OAuth backend expects so the
|
|
591
538
|
// server-side auto-cache routes correctly. text.verbosity / include /
|
|
592
539
|
// tool_choice / parallel_tool_calls are all inert without side effects
|
|
593
540
|
// for most callers but their presence affects how the OAuth backend classifies the
|
|
@@ -597,17 +544,32 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
597
544
|
const value = String(item || '').trim();
|
|
598
545
|
if (value && !include.includes(value)) include.push(value);
|
|
599
546
|
}
|
|
547
|
+
// Field order MIRRORS codex-rs ResponsesApiRequest (common.rs struct order):
|
|
548
|
+
// model, instructions, input, tools, tool_choice, parallel_tool_calls,
|
|
549
|
+
// reasoning, store, stream, include, service_tier, prompt_cache_key, text.
|
|
550
|
+
// JSON serialization order is load-bearing for the server prompt cache
|
|
551
|
+
// (exact-prefix match): matching codex's byte layout keeps our requests on
|
|
552
|
+
// the same cache-routing shape codex warms. tools/service_tier/
|
|
553
|
+
// prompt_cache_key are appended below in the same relative order.
|
|
600
554
|
const body = {
|
|
601
555
|
model,
|
|
602
556
|
instructions,
|
|
603
557
|
input,
|
|
558
|
+
tool_choice: opts.toolChoice || 'auto',
|
|
559
|
+
parallel_tool_calls: true,
|
|
560
|
+
// codex build_reasoning() sends { effort, summary } — summary defaults to
|
|
561
|
+
// ReasoningSummary::Auto (protocol config_types.rs), serialized lowercase
|
|
562
|
+
// as "auto". Matching this keeps our reasoning object byte-identical to
|
|
563
|
+
// codex so the server prompt-cache prefix hash lines up. codex also
|
|
564
|
+
// normalizes `ultra` -> `max` on the wire (reasoning_effort_for_request
|
|
565
|
+
// in core/src/client.rs); the openai-oauth backend does not accept
|
|
566
|
+
// `ultra` as a wire value, so mirror that mapping here.
|
|
567
|
+
...(_codexModelSupportsReasoningSummaries(model)
|
|
568
|
+
? { reasoning: { effort: _normalizeReasoningEffort(opts.effort), summary: 'auto' } }
|
|
569
|
+
: {}),
|
|
604
570
|
store: process.env.MIXDOG_OAI_STORE === 'true' ? true : false,
|
|
605
571
|
stream: true,
|
|
606
|
-
reasoning: { effort: opts.effort || 'medium' },
|
|
607
|
-
text: { verbosity: 'medium' },
|
|
608
572
|
include,
|
|
609
|
-
tool_choice: opts.toolChoice || 'auto',
|
|
610
|
-
parallel_tool_calls: true,
|
|
611
573
|
};
|
|
612
574
|
const maxOutputTokens = Number(opts.maxOutputTokens ?? opts.outputTokens ?? opts.max_output_tokens);
|
|
613
575
|
if (_envFlag('MIXDOG_OPENAI_OAUTH_SEND_MAX_OUTPUT_TOKENS', false)
|
|
@@ -618,28 +580,30 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
618
580
|
if (opts.fast === true) {
|
|
619
581
|
// 'priority' is the only fast-class value the OpenAI OAuth backend
|
|
620
582
|
// accepts on the wire: 'fast' is hard-rejected ("Unsupported
|
|
621
|
-
// service_tier: fast", probed 2026-06-11).
|
|
622
|
-
//
|
|
583
|
+
// service_tier: fast", probed 2026-06-11). Only send the request value
|
|
584
|
+
// when the model catalog advertises it.
|
|
623
585
|
if (codexModelSupportsServiceTier(model, 'priority')) {
|
|
624
586
|
body.service_tier = 'priority';
|
|
625
587
|
}
|
|
626
588
|
}
|
|
627
589
|
// Add tools. `nativeTools` are server-hosted Responses tools (for
|
|
628
590
|
// example web_search) and must be passed through without wrapping them as
|
|
629
|
-
// function tools.
|
|
591
|
+
// function tools. codex places `tools` right after `input` (before
|
|
592
|
+
// tool_choice); we insert it there via a rebuilt object so serialization
|
|
593
|
+
// order matches, rather than appending it last.
|
|
630
594
|
const functionTools = tools?.length ? tools.map(toOpenAIResponsesTool) : [];
|
|
631
595
|
const nativeTools = Array.isArray(opts.nativeTools)
|
|
632
596
|
? opts.nativeTools.filter(t => t && typeof t === 'object')
|
|
633
597
|
: [];
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
598
|
+
const toolsList = (functionTools.length || nativeTools.length)
|
|
599
|
+
? [...nativeTools, ...functionTools]
|
|
600
|
+
: null;
|
|
637
601
|
const promptCacheProvider = opts.promptCacheProvider || 'openai-oauth';
|
|
638
602
|
const promptCacheLane = opts.promptCacheLane || resolveProviderPromptCacheLane(promptCacheProvider, opts);
|
|
639
|
-
|
|
603
|
+
const promptCacheKey = buildStableProviderPromptCacheKey(promptCacheProvider, opts, {
|
|
640
604
|
model,
|
|
641
605
|
instructions,
|
|
642
|
-
tools:
|
|
606
|
+
tools: toolsList || [],
|
|
643
607
|
effort: body.reasoning?.effort,
|
|
644
608
|
fast: opts.fast === true,
|
|
645
609
|
serviceTier: body.service_tier || '',
|
|
@@ -648,698 +612,41 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
648
612
|
cacheLaneSlot: promptCacheLane.slot,
|
|
649
613
|
cacheLaneShards: promptCacheLane.shards,
|
|
650
614
|
});
|
|
615
|
+
// codex only serializes `text` when a verbosity/schema is configured
|
|
616
|
+
// (common.rs create_text_param_for_request returns None otherwise); it has
|
|
617
|
+
// no unconditional { verbosity: 'medium' }. Sending an extra text object
|
|
618
|
+
// shifts every following byte vs codex's prefix layout.
|
|
619
|
+
const verbosity = typeof opts.verbosity === 'string' && opts.verbosity.trim()
|
|
620
|
+
? opts.verbosity.trim().toLowerCase()
|
|
621
|
+
: null;
|
|
622
|
+
// Rebuild the body in codex struct order so JSON serialization is
|
|
623
|
+
// byte-compatible with codex: ... input, tools, tool_choice,
|
|
624
|
+
// parallel_tool_calls, reasoning, store, stream, include, service_tier,
|
|
625
|
+
// prompt_cache_key, text. service_tier is only present when fast set it.
|
|
626
|
+
const ordered = {
|
|
627
|
+
model: body.model,
|
|
628
|
+
instructions: body.instructions,
|
|
629
|
+
input: body.input,
|
|
630
|
+
...(toolsList ? { tools: toolsList } : {}),
|
|
631
|
+
tool_choice: body.tool_choice,
|
|
632
|
+
parallel_tool_calls: body.parallel_tool_calls,
|
|
633
|
+
reasoning: body.reasoning,
|
|
634
|
+
store: body.store,
|
|
635
|
+
stream: body.stream,
|
|
636
|
+
include: body.include,
|
|
637
|
+
...(body.service_tier ? { service_tier: body.service_tier } : {}),
|
|
638
|
+
prompt_cache_key: promptCacheKey,
|
|
639
|
+
...(verbosity ? { text: { verbosity } } : {}),
|
|
640
|
+
...(body.max_output_tokens ? { max_output_tokens: body.max_output_tokens } : {}),
|
|
641
|
+
};
|
|
651
642
|
// NOTE: prompt_cache_retention is a public OpenAI Responses API parameter,
|
|
652
643
|
// but the openai-oauth endpoint still rejects it ("Unsupported parameter:
|
|
653
644
|
// prompt_cache_retention", re-probed 2026-06-22). Leave retention on the
|
|
654
645
|
// openai-oauth server default; public OpenAI direct injects 24h separately.
|
|
655
|
-
return
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
function _envFlag(name, fallback = true) {
|
|
659
|
-
const raw = process.env[name];
|
|
660
|
-
if (raw == null || raw === '') return fallback;
|
|
661
|
-
return !['0', 'false', 'off', 'no'].includes(String(raw).toLowerCase());
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
function _envPositiveInt(name, fallback) {
|
|
665
|
-
const raw = process.env[name];
|
|
666
|
-
if (raw == null || raw === '') return fallback;
|
|
667
|
-
const n = Number(raw);
|
|
668
|
-
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
// Completed function_call.arguments parse for the OpenAI Responses stream.
|
|
672
|
-
// Native convergence (openai-oauth / anthropic-oauth / opencode): a function_call item
|
|
673
|
-
// arrives only on a completion/done signal, so a non-empty-but-malformed
|
|
674
|
-
// arguments string is deterministic bad JSON — NOT mid-stream truncation.
|
|
675
|
-
// Empty/whitespace input legitimately means "no arguments" → {}. A non-empty
|
|
676
|
-
// string that fails JSON.parse is surfaced as an invalid-args MARKER (instead
|
|
677
|
-
// of being silently swallowed to {}) so the dispatch loop turns it into an
|
|
678
|
-
// is_error tool_result and the model self-corrects in the same turn.
|
|
679
|
-
function _parseJsonObject(value) {
|
|
680
|
-
const text = typeof value === 'string' ? value : (value == null ? '' : String(value));
|
|
681
|
-
if (text.trim() === '') return {};
|
|
682
|
-
try {
|
|
683
|
-
const parsed = JSON.parse(text);
|
|
684
|
-
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
685
|
-
} catch (err) {
|
|
686
|
-
return makeInvalidToolArgsMarker(text, err instanceof Error ? err.message : String(err));
|
|
687
|
-
}
|
|
646
|
+
return ordered;
|
|
688
647
|
}
|
|
689
648
|
|
|
690
|
-
|
|
691
|
-
const details = usage?.input_tokens_details || usage?.prompt_tokens_details || {};
|
|
692
|
-
return Number(details.cached_tokens ?? details.cached ?? usage?.cached_tokens ?? 0) || 0;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
function _sseEventsFromBuffer(buffer) {
|
|
696
|
-
const frames = [];
|
|
697
|
-
let rest = buffer.replace(/\r\n/g, '\n');
|
|
698
|
-
let idx;
|
|
699
|
-
while ((idx = rest.indexOf('\n\n')) >= 0) {
|
|
700
|
-
frames.push(rest.slice(0, idx));
|
|
701
|
-
rest = rest.slice(idx + 2);
|
|
702
|
-
}
|
|
703
|
-
return { frames, rest };
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
function _parseSseFrame(frame) {
|
|
707
|
-
const lines = String(frame || '').split('\n');
|
|
708
|
-
const data = [];
|
|
709
|
-
for (const line of lines) {
|
|
710
|
-
if (!line || line.startsWith(':')) continue;
|
|
711
|
-
if (line.startsWith('data:')) data.push(line.slice(5).trimStart());
|
|
712
|
-
}
|
|
713
|
-
if (!data.length) return null;
|
|
714
|
-
const raw = data.join('\n').trim();
|
|
715
|
-
if (!raw || raw === '[DONE]') return null;
|
|
716
|
-
try { return JSON.parse(raw); } catch { return null; }
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
function _incompleteReasonFromEvent(event) {
|
|
720
|
-
const reasonObj = event?.response?.incomplete_details
|
|
721
|
-
|| event?.incomplete_details
|
|
722
|
-
|| event?.response?.status_details
|
|
723
|
-
|| null;
|
|
724
|
-
return String(reasonObj?.reason || event?.response?.status || 'incomplete');
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
function _isMaxOutputIncompleteReason(reason) {
|
|
728
|
-
return /^(?:max_output_tokens|max_tokens|length|output_token_limit)$/i.test(String(reason || '').trim());
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
function _pushOutputTextAnnotations(part, citations, citationKeys) {
|
|
732
|
-
const annotations = Array.isArray(part?.annotations) ? part.annotations : [];
|
|
733
|
-
for (const raw of annotations) {
|
|
734
|
-
const url = raw?.url || raw?.uri || raw?.href || '';
|
|
735
|
-
if (!url || citationKeys.has(url)) continue;
|
|
736
|
-
citationKeys.add(url);
|
|
737
|
-
citations.push({
|
|
738
|
-
title: raw?.title || '',
|
|
739
|
-
url,
|
|
740
|
-
snippet: raw?.snippet || raw?.text || raw?.description || '',
|
|
741
|
-
source: 'openai-oauth',
|
|
742
|
-
});
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
function _buildOpenAIHttpFallbackHeaders({ auth, cacheKey }) {
|
|
747
|
-
const headers = {
|
|
748
|
-
Authorization: `Bearer ${auth.access_token}`,
|
|
749
|
-
'Content-Type': 'application/json',
|
|
750
|
-
Accept: 'text/event-stream',
|
|
751
|
-
'OpenAI-Beta': 'responses=experimental',
|
|
752
|
-
originator: CODEX_OAUTH_ORIGINATOR,
|
|
753
|
-
'chatgpt-account-id': auth.account_id || '',
|
|
754
|
-
'x-client-request-id': randomBytes(16).toString('hex'),
|
|
755
|
-
};
|
|
756
|
-
if (cacheKey) headers.session_id = String(cacheKey);
|
|
757
|
-
return headers;
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
// WS→HTTP/SSE fallback predicate → shared shouldFallbackTransport
|
|
761
|
-
// (retry-classifier.mjs). The per-provider env flag is computed here and passed
|
|
762
|
-
// as `enabled`; the deny-order + allow-list are identical to the former copy.
|
|
763
|
-
function _shouldUseOpenAIHttpFallback(err, externalSignal) {
|
|
764
|
-
return shouldFallbackTransport(err, {
|
|
765
|
-
signal: externalSignal,
|
|
766
|
-
enabled: _envFlag('MIXDOG_OPENAI_OAUTH_HTTP_FALLBACK', true),
|
|
767
|
-
});
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
// Exported for the single-emit regression smoke (scripts/openai-oauth-
|
|
771
|
-
// http-sse-toolcall-smoke.mjs): the SSE stream can surface the same
|
|
772
|
-
// function_call across response.function_call_arguments.done +
|
|
773
|
-
// response.output_item.done + response.completed, and onToolCall must fire
|
|
774
|
-
// exactly once per call id. No production caller imports this name; the
|
|
775
|
-
// provider invokes it internally.
|
|
776
|
-
export async function sendViaHttpSse({
|
|
777
|
-
auth,
|
|
778
|
-
body,
|
|
779
|
-
opts,
|
|
780
|
-
onStreamDelta,
|
|
781
|
-
onToolCall,
|
|
782
|
-
onTextDelta,
|
|
783
|
-
onStageChange,
|
|
784
|
-
externalSignal,
|
|
785
|
-
poolKey,
|
|
786
|
-
cacheKey,
|
|
787
|
-
iteration,
|
|
788
|
-
useModel,
|
|
789
|
-
fetchFn = fetch,
|
|
790
|
-
} = {}) {
|
|
791
|
-
// P1 audit fix: no fixed wall-clock total cap on the HTTP/SSE fallback
|
|
792
|
-
// stream. The old createTimeoutSignal(..., PROVIDER_GENERATE_TOTAL_TIMEOUT_MS)
|
|
793
|
-
// killed a healthy, still-streaming turn purely on elapsed time, unlike
|
|
794
|
-
// every other streaming provider path (anthropic-oauth uses the same
|
|
795
|
-
// createPassthroughSignal pattern — see anthropic-oauth.mjs "Option A").
|
|
796
|
-
// The stream is bounded instead by:
|
|
797
|
-
// (a) headerTimeout below (PROVIDER_HTTP_RESPONSE_TIMEOUT_MS) for a
|
|
798
|
-
// socket that never sends the initial response,
|
|
799
|
-
// (b) the SEMANTIC idle watchdog (_armSemanticIdle /
|
|
800
|
-
// PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS), which resets on every
|
|
801
|
-
// meaningful() chunk — a live stream stays alive, a truly silent
|
|
802
|
-
// one still aborts, and
|
|
803
|
-
// (c) externalSignal (client disconnect / replaced-by-newer-request).
|
|
804
|
-
const totalTimeout = createPassthroughSignal(externalSignal);
|
|
805
|
-
const headerTimeout = createTimeoutSignal(
|
|
806
|
-
totalTimeout.signal,
|
|
807
|
-
PROVIDER_HTTP_RESPONSE_TIMEOUT_MS,
|
|
808
|
-
'OpenAI OAuth HTTP fallback initial response',
|
|
809
|
-
);
|
|
810
|
-
const headers = _buildOpenAIHttpFallbackHeaders({ auth, cacheKey });
|
|
811
|
-
const fetchStartedAt = Date.now();
|
|
812
|
-
let response;
|
|
813
|
-
try {
|
|
814
|
-
try { onStageChange?.('requesting'); } catch {}
|
|
815
|
-
response = await fetchFn(CODEX_RESPONSES_URL, {
|
|
816
|
-
method: 'POST',
|
|
817
|
-
headers,
|
|
818
|
-
body: JSON.stringify(body),
|
|
819
|
-
signal: headerTimeout.signal,
|
|
820
|
-
dispatcher: getLlmDispatcher(),
|
|
821
|
-
});
|
|
822
|
-
} catch (err) {
|
|
823
|
-
if (headerTimeout.signal?.aborted && headerTimeout.signal.reason instanceof Error) throw headerTimeout.signal.reason;
|
|
824
|
-
throw err;
|
|
825
|
-
} finally {
|
|
826
|
-
headerTimeout.cleanup();
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
traceAgentFetch({
|
|
830
|
-
sessionId: poolKey,
|
|
831
|
-
headersMs: Date.now() - fetchStartedAt,
|
|
832
|
-
httpStatus: response.status,
|
|
833
|
-
provider: 'openai-oauth',
|
|
834
|
-
model: useModel,
|
|
835
|
-
transport: 'http',
|
|
836
|
-
});
|
|
837
|
-
|
|
838
|
-
if (!response.ok) {
|
|
839
|
-
const text = await response.text().catch(() => '');
|
|
840
|
-
const err = new Error(`OpenAI OAuth HTTP fallback ${response.status}: ${text.slice(0, 200)}`);
|
|
841
|
-
err.httpStatus = response.status;
|
|
842
|
-
err.headers = response.headers;
|
|
843
|
-
populateHttpStatusFromMessage(err, text);
|
|
844
|
-
totalTimeout.cleanup();
|
|
845
|
-
throw err;
|
|
846
|
-
}
|
|
847
|
-
if (!response.body) {
|
|
848
|
-
totalTimeout.cleanup();
|
|
849
|
-
throw new Error('OpenAI OAuth HTTP fallback returned no response body');
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
try { onStageChange?.('streaming'); } catch {}
|
|
853
|
-
const sseStartedAt = Date.now();
|
|
854
|
-
const reader = response.body.getReader();
|
|
855
|
-
const decoder = new TextDecoder();
|
|
856
|
-
// After headerTimeout.cleanup() the in-flight fetch no longer carries a live
|
|
857
|
-
// signal, so a totalTimeout / external abort that fires during a pending
|
|
858
|
-
// reader.read() would otherwise leave the pooled request hanging. Keep the
|
|
859
|
-
// reader tied to totalTimeout for the whole stream: on abort, cancel the
|
|
860
|
-
// reader so the awaited read() unblocks and the socket is released back to
|
|
861
|
-
// the shared pool instead of leaking. reader.cancel() may resolve the
|
|
862
|
-
// pending read() as {done:true} rather than rejecting, which would let a
|
|
863
|
-
// partial response surface as success — so record the abort reason and
|
|
864
|
-
// re-throw it after the loop unblocks (see below).
|
|
865
|
-
let _streamAbortReason = null;
|
|
866
|
-
let _onTotalAbort = null;
|
|
867
|
-
if (totalTimeout.signal) {
|
|
868
|
-
_onTotalAbort = () => {
|
|
869
|
-
const reason = totalTimeout.signal.reason;
|
|
870
|
-
_streamAbortReason = reason instanceof Error
|
|
871
|
-
? reason
|
|
872
|
-
: new Error('OpenAI OAuth HTTP fallback aborted');
|
|
873
|
-
try { reader.cancel(_streamAbortReason).catch(() => {}); } catch {}
|
|
874
|
-
};
|
|
875
|
-
if (totalTimeout.signal.aborted) _onTotalAbort();
|
|
876
|
-
else totalTimeout.signal.addEventListener('abort', _onTotalAbort, { once: true });
|
|
877
|
-
}
|
|
878
|
-
// SEMANTIC idle watchdog: reset ONLY on meaningful() (text/reasoning/tool
|
|
879
|
-
// deltas), never on raw bytes/keepalive frames, so a stream that emits some
|
|
880
|
-
// deltas then goes silent trips a short, named terminal failure instead of
|
|
881
|
-
// hanging until the 30-min agent watchdog. Disablable via the shared env.
|
|
882
|
-
let _semanticIdleTimer = null;
|
|
883
|
-
const _clearSemanticIdle = () => {
|
|
884
|
-
if (_semanticIdleTimer) { clearTimeout(_semanticIdleTimer); _semanticIdleTimer = null; }
|
|
885
|
-
};
|
|
886
|
-
const _armSemanticIdle = () => {
|
|
887
|
-
if (!PROVIDER_SSE_IDLE_WATCHDOG_ENABLED || !(PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS > 0)) return;
|
|
888
|
-
_clearSemanticIdle();
|
|
889
|
-
_semanticIdleTimer = setTimeout(() => {
|
|
890
|
-
_streamAbortReason = streamStalledError('OpenAI OAuth HTTP fallback', PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS, { emittedToolCall: emittedToolCallIds.size > 0 });
|
|
891
|
-
// Partial-final recovery parity with anthropic-oauth: attach the
|
|
892
|
-
// streamed partial state so the agent loop can accept a wedged FINAL
|
|
893
|
-
// no-tool summary as a successful partial-final instead of dropping
|
|
894
|
-
// the result. pendingToolUse gates out any mid-flight tool call.
|
|
895
|
-
try {
|
|
896
|
-
_streamAbortReason.partialContent = content;
|
|
897
|
-
_streamAbortReason.partialToolCalls = toolCalls.length ? toolCalls.slice() : undefined;
|
|
898
|
-
_streamAbortReason.pendingToolUse = pendingCalls.size > 0 || emittedToolCallIds.size > 0;
|
|
899
|
-
_streamAbortReason.partialModel = model || undefined;
|
|
900
|
-
} catch { /* best-effort enrichment */ }
|
|
901
|
-
try { reader.cancel(_streamAbortReason).catch(() => {}); } catch {}
|
|
902
|
-
}, PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS);
|
|
903
|
-
try { _semanticIdleTimer.unref?.(); } catch {}
|
|
904
|
-
};
|
|
905
|
-
let buffer = '';
|
|
906
|
-
let content = '';
|
|
907
|
-
let model = '';
|
|
908
|
-
let responseId = '';
|
|
909
|
-
let serviceTier = '';
|
|
910
|
-
let usage = null;
|
|
911
|
-
let ttftMs = null;
|
|
912
|
-
const toolCalls = [];
|
|
913
|
-
const pendingCalls = new Map();
|
|
914
|
-
const reasoningItems = [];
|
|
915
|
-
const citations = [];
|
|
916
|
-
const citationKeys = new Set();
|
|
917
|
-
const webSearchCalls = [];
|
|
918
|
-
const webSearchCallKeys = new Set();
|
|
919
|
-
let completed = false;
|
|
920
|
-
let stopReason = null;
|
|
921
|
-
// Gateway live-text relay invariant: set once a non-empty text chunk has
|
|
922
|
-
// been forwarded to the client. A failure afterwards is non-retryable —
|
|
923
|
-
// the rendered text cannot be withdrawn and a re-request would concatenate
|
|
924
|
-
// a second attempt.
|
|
925
|
-
let emittedText = false;
|
|
926
|
-
|
|
927
|
-
// Single-emit guard for tool calls (matches the WS path's
|
|
928
|
-
// emittedToolCall intent). The HTTP/SSE event stream can surface the
|
|
929
|
-
// same function_call across multiple frames — response.function_call_arguments.done,
|
|
930
|
-
// response.output_item.done, and the final response.completed.output
|
|
931
|
-
// bundle. Each frame independently completes the call (id + name) and
|
|
932
|
-
// would re-invoke onToolCall, double-executing a side-effecting tool.
|
|
933
|
-
// Route every emit through emitToolCall: it fires the callback exactly
|
|
934
|
-
// once per unique call id, the first time the call is complete. A call
|
|
935
|
-
// whose id/name only arrives in a later frame is NOT dropped — its
|
|
936
|
-
// first complete frame still emits; only redundant re-emits are
|
|
937
|
-
// suppressed.
|
|
938
|
-
const emittedToolCallIds = new Set();
|
|
939
|
-
// Fix 2: cross-path name+args dedupe. A text-leaked synthetic and an
|
|
940
|
-
// identical native function_call must fire onToolCall exactly once.
|
|
941
|
-
const _toolDedupe = createToolCallDedupe();
|
|
942
|
-
const emitToolCall = (call) => {
|
|
943
|
-
if (!call || !call.id) return;
|
|
944
|
-
if (emittedToolCallIds.has(call.id)) return;
|
|
945
|
-
emittedToolCallIds.add(call.id);
|
|
946
|
-
if (!_toolDedupe.shouldDispatch(call.name, call.arguments)) return;
|
|
947
|
-
try { onToolCall?.(call); } catch {}
|
|
948
|
-
};
|
|
949
|
-
|
|
950
|
-
// Leaked tool-call guard. The model sometimes emits a tool call as plain
|
|
951
|
-
// text (XML `<invoke>`/`<function_calls>` or gpt-oss harmony
|
|
952
|
-
// `<|channel|>...to=functions.NAME...<|call|>`) inside
|
|
953
|
-
// `response.output_text.delta` instead of a native function_call. Route
|
|
954
|
-
// text through the guard so leaked calls are suppressed from the visible
|
|
955
|
-
// stream, synthesized (native `call_...` id shape), and dispatched like
|
|
956
|
-
// native ones. Known tool names come from the request body so recovery
|
|
957
|
-
// only fires for tools the model was actually offered. Additive: the
|
|
958
|
-
// native function_call path is untouched.
|
|
959
|
-
const _leakKnownTools = new Set(
|
|
960
|
-
(Array.isArray(body?.tools) ? body.tools : [])
|
|
961
|
-
.map((t) => (typeof t?.name === 'string' ? t.name : null))
|
|
962
|
-
.filter(Boolean),
|
|
963
|
-
);
|
|
964
|
-
const leakGuard = createLeakGuard({ knownToolNames: _leakKnownTools, harmony: true });
|
|
965
|
-
const dispatchLeakedCall = (recovered) => {
|
|
966
|
-
let args = recovered?.arguments;
|
|
967
|
-
if (args === null || typeof args !== 'object' || Array.isArray(args)) args = {};
|
|
968
|
-
const call = {
|
|
969
|
-
id: `call_leaked_${randomBytes(8).toString('hex')}`,
|
|
970
|
-
name: recovered.name,
|
|
971
|
-
arguments: args,
|
|
972
|
-
};
|
|
973
|
-
toolCalls.push(call);
|
|
974
|
-
emitToolCall(call);
|
|
975
|
-
};
|
|
976
|
-
const relayLeakText = (delta) => {
|
|
977
|
-
if (!leakGuard.enabled) {
|
|
978
|
-
content += delta || '';
|
|
979
|
-
if (delta && onTextDelta) {
|
|
980
|
-
emittedText = true;
|
|
981
|
-
try { onTextDelta(delta); } catch {}
|
|
982
|
-
}
|
|
983
|
-
return;
|
|
984
|
-
}
|
|
985
|
-
const { text, calls } = leakGuard.push(delta);
|
|
986
|
-
if (text) {
|
|
987
|
-
content += text;
|
|
988
|
-
if (onTextDelta) {
|
|
989
|
-
emittedText = true;
|
|
990
|
-
try { onTextDelta(text); } catch {}
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
for (const c of calls) dispatchLeakedCall(c);
|
|
994
|
-
};
|
|
995
|
-
const flushLeak = () => {
|
|
996
|
-
if (!leakGuard.enabled) return;
|
|
997
|
-
const { text, calls } = leakGuard.flush();
|
|
998
|
-
if (text) {
|
|
999
|
-
content += text;
|
|
1000
|
-
if (onTextDelta) {
|
|
1001
|
-
emittedText = true;
|
|
1002
|
-
try { onTextDelta(text); } catch {}
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
for (const c of calls) dispatchLeakedCall(c);
|
|
1006
|
-
};
|
|
1007
|
-
|
|
1008
|
-
const pushWebSearchCall = (item) => {
|
|
1009
|
-
if (!item || item.type !== 'web_search_call') return;
|
|
1010
|
-
const key = item.id || JSON.stringify(item.action || item);
|
|
1011
|
-
if (webSearchCallKeys.has(key)) return;
|
|
1012
|
-
webSearchCallKeys.add(key);
|
|
1013
|
-
webSearchCalls.push({ id: item.id || '', status: item.status || '', action: item.action || null });
|
|
1014
|
-
};
|
|
1015
|
-
const pushReasoningItem = (item) => {
|
|
1016
|
-
if (item?.type === 'reasoning' && item.encrypted_content && !reasoningItems.some(r => r.id === item.id)) {
|
|
1017
|
-
reasoningItems.push({
|
|
1018
|
-
id: item.id || '',
|
|
1019
|
-
encrypted_content: item.encrypted_content,
|
|
1020
|
-
summary: Array.isArray(item.summary) ? item.summary : [],
|
|
1021
|
-
});
|
|
1022
|
-
}
|
|
1023
|
-
};
|
|
1024
|
-
const pushToolSearchCall = (item) => {
|
|
1025
|
-
if (!item || item.type !== 'tool_search_call') return;
|
|
1026
|
-
const callId = item.call_id || item.id || '';
|
|
1027
|
-
if (!callId || toolCalls.some(t => t.id === callId)) return;
|
|
1028
|
-
let args = {};
|
|
1029
|
-
if (item.arguments && typeof item.arguments === 'object') {
|
|
1030
|
-
args = item.arguments;
|
|
1031
|
-
} else if (typeof item.arguments === 'string' && item.arguments.trim()) {
|
|
1032
|
-
// Non-empty but malformed tool_search arguments are deterministic
|
|
1033
|
-
// bad JSON (the item is only emitted on completion). Surface an
|
|
1034
|
-
// invalid-args marker instead of swallowing to {} so the model can
|
|
1035
|
-
// self-correct in the same turn.
|
|
1036
|
-
args = _parseJsonObject(item.arguments);
|
|
1037
|
-
}
|
|
1038
|
-
const call = {
|
|
1039
|
-
id: callId,
|
|
1040
|
-
name: 'tool_search',
|
|
1041
|
-
arguments: args,
|
|
1042
|
-
nativeType: 'tool_search_call',
|
|
1043
|
-
};
|
|
1044
|
-
toolCalls.push(call);
|
|
1045
|
-
emitToolCall(call);
|
|
1046
|
-
};
|
|
1047
|
-
const pushCustomToolCall = (item) => {
|
|
1048
|
-
const call = customToolCallFromResponseItem(item);
|
|
1049
|
-
if (!call || toolCalls.some(t => t.id === call.id)) return;
|
|
1050
|
-
toolCalls.push(call);
|
|
1051
|
-
emitToolCall(call);
|
|
1052
|
-
};
|
|
1053
|
-
const meaningful = () => {
|
|
1054
|
-
if (ttftMs == null) ttftMs = Date.now() - sseStartedAt;
|
|
1055
|
-
_armSemanticIdle();
|
|
1056
|
-
try { onStreamDelta?.(); } catch {}
|
|
1057
|
-
};
|
|
1058
|
-
const handleEvent = (event) => {
|
|
1059
|
-
if (!event || typeof event.type !== 'string') return;
|
|
1060
|
-
switch (event.type) {
|
|
1061
|
-
case 'response.created':
|
|
1062
|
-
if (event.response?.model) model = event.response.model;
|
|
1063
|
-
if (event.response?.id) responseId = event.response.id;
|
|
1064
|
-
break;
|
|
1065
|
-
case 'response.output_text.delta':
|
|
1066
|
-
meaningful();
|
|
1067
|
-
relayLeakText(event.delta || '');
|
|
1068
|
-
break;
|
|
1069
|
-
case 'response.reasoning_text.delta':
|
|
1070
|
-
case 'response.reasoning_summary_text.delta':
|
|
1071
|
-
meaningful();
|
|
1072
|
-
break;
|
|
1073
|
-
case 'response.output_item.added':
|
|
1074
|
-
if (event.item?.type === 'function_call') {
|
|
1075
|
-
pendingCalls.set(event.item.id || '', {
|
|
1076
|
-
name: event.item.name || '',
|
|
1077
|
-
callId: event.item.call_id || '',
|
|
1078
|
-
});
|
|
1079
|
-
}
|
|
1080
|
-
break;
|
|
1081
|
-
case 'response.function_call_arguments.delta':
|
|
1082
|
-
meaningful();
|
|
1083
|
-
break;
|
|
1084
|
-
case 'response.function_call_arguments.done': {
|
|
1085
|
-
const itemId = event.item_id || '';
|
|
1086
|
-
const pending = pendingCalls.get(itemId);
|
|
1087
|
-
const call = {
|
|
1088
|
-
id: pending?.callId || event.call_id || '',
|
|
1089
|
-
name: pending?.name || event.name || '',
|
|
1090
|
-
arguments: _parseJsonObject(event.arguments),
|
|
1091
|
-
_pendingItemId: itemId,
|
|
1092
|
-
};
|
|
1093
|
-
toolCalls.push(call);
|
|
1094
|
-
if (call.id && call.name) {
|
|
1095
|
-
delete call._pendingItemId;
|
|
1096
|
-
emitToolCall(call);
|
|
1097
|
-
}
|
|
1098
|
-
meaningful();
|
|
1099
|
-
break;
|
|
1100
|
-
}
|
|
1101
|
-
case 'response.custom_tool_call_input.delta':
|
|
1102
|
-
meaningful();
|
|
1103
|
-
break;
|
|
1104
|
-
case 'response.output_item.done': {
|
|
1105
|
-
const item = event.item || {};
|
|
1106
|
-
pushReasoningItem(item);
|
|
1107
|
-
pushWebSearchCall(item);
|
|
1108
|
-
if (item.type === 'function_call') {
|
|
1109
|
-
const tc = toolCalls.find(t => t._pendingItemId === (item.id || ''));
|
|
1110
|
-
if (tc) {
|
|
1111
|
-
if (!tc.id && item.call_id) tc.id = item.call_id;
|
|
1112
|
-
if (!tc.name && item.name) tc.name = item.name;
|
|
1113
|
-
if (tc.id && tc.name) {
|
|
1114
|
-
delete tc._pendingItemId;
|
|
1115
|
-
emitToolCall(tc);
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
} else if (item.type === 'tool_search_call') {
|
|
1119
|
-
pushToolSearchCall(item);
|
|
1120
|
-
} else if (item.type === 'custom_tool_call') {
|
|
1121
|
-
pushCustomToolCall(item);
|
|
1122
|
-
meaningful();
|
|
1123
|
-
}
|
|
1124
|
-
break;
|
|
1125
|
-
}
|
|
1126
|
-
case 'response.completed': {
|
|
1127
|
-
const resp = event.response || {};
|
|
1128
|
-
serviceTier = resp.service_tier || resp.serviceTier || serviceTier;
|
|
1129
|
-
if (!model && resp.model) model = resp.model;
|
|
1130
|
-
if (!responseId && resp.id) responseId = resp.id;
|
|
1131
|
-
if (resp.usage) {
|
|
1132
|
-
usage = {
|
|
1133
|
-
inputTokens: resp.usage.input_tokens || 0,
|
|
1134
|
-
outputTokens: resp.usage.output_tokens || 0,
|
|
1135
|
-
cachedTokens: _extractCachedTokens(resp.usage),
|
|
1136
|
-
promptTokens: resp.usage.input_tokens || 0,
|
|
1137
|
-
raw: serviceTier ? { ...resp.usage, service_tier: serviceTier } : resp.usage,
|
|
1138
|
-
};
|
|
1139
|
-
}
|
|
1140
|
-
for (const item of resp.output || []) {
|
|
1141
|
-
if (item.type === 'message') {
|
|
1142
|
-
for (const part of item.content || []) {
|
|
1143
|
-
if (!content && part.type === 'output_text') {
|
|
1144
|
-
// Completed-output fallback (no streamed text).
|
|
1145
|
-
// Route through the leak guard so a tool call
|
|
1146
|
-
// leaked only in the final bundle is recovered
|
|
1147
|
-
// rather than surfaced as visible content. push
|
|
1148
|
-
// with final=true flushes fully (no held tail).
|
|
1149
|
-
if (leakGuard.enabled) {
|
|
1150
|
-
const { text, calls } = leakGuard.push(part.text || '', true);
|
|
1151
|
-
content += text;
|
|
1152
|
-
for (const c of calls) dispatchLeakedCall(c);
|
|
1153
|
-
} else {
|
|
1154
|
-
content += part.text || '';
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
if (part.type === 'output_text') _pushOutputTextAnnotations(part, citations, citationKeys);
|
|
1158
|
-
}
|
|
1159
|
-
} else if (item.type === 'reasoning') {
|
|
1160
|
-
pushReasoningItem(item);
|
|
1161
|
-
} else if (item.type === 'web_search_call') {
|
|
1162
|
-
pushWebSearchCall(item);
|
|
1163
|
-
} else if (item.type === 'tool_search_call') {
|
|
1164
|
-
pushToolSearchCall(item);
|
|
1165
|
-
} else if (item.type === 'custom_tool_call') {
|
|
1166
|
-
pushCustomToolCall(item);
|
|
1167
|
-
meaningful();
|
|
1168
|
-
} else if (item.type === 'function_call') {
|
|
1169
|
-
// Match the still-pending placeholder by item id, or
|
|
1170
|
-
// an already-recorded call by its canonical call_id —
|
|
1171
|
-
// so a call completed at args.done / output_item.done
|
|
1172
|
-
// is reused here rather than re-pushed as a duplicate.
|
|
1173
|
-
const tc = toolCalls.find(t =>
|
|
1174
|
-
t._pendingItemId === (item.id || '')
|
|
1175
|
-
|| (item.call_id && t.id === item.call_id));
|
|
1176
|
-
if (tc) {
|
|
1177
|
-
if (!tc.id && item.call_id) tc.id = item.call_id;
|
|
1178
|
-
if (!tc.name && item.name) tc.name = item.name;
|
|
1179
|
-
if (tc.id && tc.name) {
|
|
1180
|
-
delete tc._pendingItemId;
|
|
1181
|
-
emitToolCall(tc);
|
|
1182
|
-
}
|
|
1183
|
-
} else if (item.call_id && item.name) {
|
|
1184
|
-
const call = {
|
|
1185
|
-
id: item.call_id,
|
|
1186
|
-
name: item.name,
|
|
1187
|
-
arguments: _parseJsonObject(item.arguments),
|
|
1188
|
-
};
|
|
1189
|
-
toolCalls.push(call);
|
|
1190
|
-
emitToolCall(call);
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
completed = true;
|
|
1195
|
-
break;
|
|
1196
|
-
}
|
|
1197
|
-
case 'response.done':
|
|
1198
|
-
if (!event.response || event.response.status === 'completed') completed = true;
|
|
1199
|
-
else if (event.response.status === 'failed') {
|
|
1200
|
-
const msg = event.response?.error?.message || 'response.done failed';
|
|
1201
|
-
const err = new Error(`OpenAI OAuth HTTP fallback response.done failed: ${msg}`);
|
|
1202
|
-
populateHttpStatusFromMessage(err, msg);
|
|
1203
|
-
throw err;
|
|
1204
|
-
} else if (event.response.status === 'incomplete') {
|
|
1205
|
-
const reason = _incompleteReasonFromEvent(event);
|
|
1206
|
-
if (_isMaxOutputIncompleteReason(reason)) {
|
|
1207
|
-
completed = true;
|
|
1208
|
-
stopReason = 'length';
|
|
1209
|
-
break;
|
|
1210
|
-
}
|
|
1211
|
-
throw new Error(`OpenAI OAuth HTTP fallback response.done incomplete: ${reason}`);
|
|
1212
|
-
}
|
|
1213
|
-
break;
|
|
1214
|
-
case 'response.failed': {
|
|
1215
|
-
const msg = event.response?.error?.message || event.error?.message || event.message || 'response.failed';
|
|
1216
|
-
const err = new Error(`OpenAI OAuth HTTP fallback response.failed: ${msg}`);
|
|
1217
|
-
populateHttpStatusFromMessage(err, msg);
|
|
1218
|
-
throw err;
|
|
1219
|
-
}
|
|
1220
|
-
case 'response.incomplete': {
|
|
1221
|
-
const reason = _incompleteReasonFromEvent(event);
|
|
1222
|
-
if (_isMaxOutputIncompleteReason(reason)) {
|
|
1223
|
-
completed = true;
|
|
1224
|
-
stopReason = 'length';
|
|
1225
|
-
break;
|
|
1226
|
-
}
|
|
1227
|
-
throw new Error(`OpenAI OAuth HTTP fallback response.incomplete: ${reason}`);
|
|
1228
|
-
}
|
|
1229
|
-
case 'error': {
|
|
1230
|
-
const msg = event.message || event.error?.message || 'unknown';
|
|
1231
|
-
const err = new Error(`OpenAI OAuth HTTP fallback error: ${msg}`);
|
|
1232
|
-
populateHttpStatusFromMessage(err, msg);
|
|
1233
|
-
throw err;
|
|
1234
|
-
}
|
|
1235
|
-
default:
|
|
1236
|
-
break;
|
|
1237
|
-
}
|
|
1238
|
-
};
|
|
1239
|
-
|
|
1240
|
-
try {
|
|
1241
|
-
while (true) {
|
|
1242
|
-
if (totalTimeout.signal?.aborted) {
|
|
1243
|
-
_clearSemanticIdle();
|
|
1244
|
-
const reason = totalTimeout.signal.reason;
|
|
1245
|
-
throw reason instanceof Error ? reason : new Error('OpenAI OAuth HTTP fallback aborted');
|
|
1246
|
-
}
|
|
1247
|
-
if (_streamAbortReason) throw _streamAbortReason;
|
|
1248
|
-
const { value, done } = await reader.read();
|
|
1249
|
-
if (done) break;
|
|
1250
|
-
buffer += decoder.decode(value, { stream: true });
|
|
1251
|
-
const parsed = _sseEventsFromBuffer(buffer);
|
|
1252
|
-
buffer = parsed.rest;
|
|
1253
|
-
for (const frame of parsed.frames) {
|
|
1254
|
-
const event = _parseSseFrame(frame);
|
|
1255
|
-
if (event) handleEvent(event);
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
// The read() above can unblock via reader.cancel() as {done:true} on an
|
|
1259
|
-
// external/total-timeout abort. Surface that as the abort/timeout error
|
|
1260
|
-
// instead of treating the partial stream as a successful response.
|
|
1261
|
-
if (_streamAbortReason) throw _streamAbortReason;
|
|
1262
|
-
buffer += decoder.decode();
|
|
1263
|
-
const parsed = _sseEventsFromBuffer(buffer + '\n\n');
|
|
1264
|
-
for (const frame of parsed.frames) {
|
|
1265
|
-
const event = _parseSseFrame(frame);
|
|
1266
|
-
if (event) handleEvent(event);
|
|
1267
|
-
}
|
|
1268
|
-
// Flush any partial-sentinel tail held back mid-stream so legitimate
|
|
1269
|
-
// trailing text is never lost (streamed-text path).
|
|
1270
|
-
flushLeak();
|
|
1271
|
-
} catch (err) {
|
|
1272
|
-
// Live-text invariant: once a non-empty chunk has been relayed it
|
|
1273
|
-
// cannot be withdrawn — flag the error so no upstream layer retries.
|
|
1274
|
-
if (emittedText && err) { try { err.liveTextEmitted = true; err.unsafeToRetry = true; } catch {} }
|
|
1275
|
-
throw err;
|
|
1276
|
-
} finally {
|
|
1277
|
-
_clearSemanticIdle();
|
|
1278
|
-
try { reader.releaseLock?.(); } catch {}
|
|
1279
|
-
if (_onTotalAbort && totalTimeout.signal) {
|
|
1280
|
-
try { totalTimeout.signal.removeEventListener('abort', _onTotalAbort); } catch {}
|
|
1281
|
-
}
|
|
1282
|
-
totalTimeout.cleanup();
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
const unresolved = toolCalls.find(t => t._pendingItemId);
|
|
1286
|
-
if (unresolved) {
|
|
1287
|
-
throw new Error(`OpenAI OAuth HTTP fallback function_call salvage failed: missing call_id/name for item_id=${unresolved._pendingItemId || '?'}`);
|
|
1288
|
-
}
|
|
1289
|
-
if (!completed && !content && !toolCalls.length) {
|
|
1290
|
-
throw new Error('OpenAI OAuth HTTP fallback ended before response.completed');
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
const liveModel = model || useModel;
|
|
1294
|
-
traceAgentSse({
|
|
1295
|
-
sessionId: poolKey,
|
|
1296
|
-
sseParseMs: Date.now() - sseStartedAt,
|
|
1297
|
-
ttftMs,
|
|
1298
|
-
provider: 'openai-oauth',
|
|
1299
|
-
model: liveModel,
|
|
1300
|
-
transport: 'sse',
|
|
1301
|
-
});
|
|
1302
|
-
if (usage) {
|
|
1303
|
-
traceAgentUsage({
|
|
1304
|
-
sessionId: poolKey,
|
|
1305
|
-
iteration,
|
|
1306
|
-
inputTokens: usage.inputTokens || 0,
|
|
1307
|
-
outputTokens: usage.outputTokens || 0,
|
|
1308
|
-
cachedTokens: usage.cachedTokens || 0,
|
|
1309
|
-
promptTokens: usage.promptTokens || 0,
|
|
1310
|
-
model: liveModel,
|
|
1311
|
-
modelDisplay: _displayCodexModel(liveModel),
|
|
1312
|
-
responseId: responseId || null,
|
|
1313
|
-
rawUsage: usage.raw || null,
|
|
1314
|
-
provider: 'openai-oauth',
|
|
1315
|
-
serviceTier,
|
|
1316
|
-
});
|
|
1317
|
-
}
|
|
1318
|
-
// Dedupe the returned array by name+args (Fix 2, array side): a synthetic
|
|
1319
|
-
// leaked call and an identical native function_call must not both survive,
|
|
1320
|
-
// else the agent loop executes the side-effecting tool twice.
|
|
1321
|
-
const _returnedToolCalls = toolCalls.length
|
|
1322
|
-
? dedupeToolCallList(toolCalls.map(({ _pendingItemId, ...t }) => t))
|
|
1323
|
-
: undefined;
|
|
1324
|
-
return {
|
|
1325
|
-
content,
|
|
1326
|
-
model: liveModel,
|
|
1327
|
-
reasoningItems: reasoningItems.length ? reasoningItems : undefined,
|
|
1328
|
-
toolCalls: _returnedToolCalls,
|
|
1329
|
-
citations: citations.length ? citations : undefined,
|
|
1330
|
-
webSearchCalls: webSearchCalls.length ? webSearchCalls : undefined,
|
|
1331
|
-
usage: usage || undefined,
|
|
1332
|
-
stopReason: stopReason || undefined,
|
|
1333
|
-
// P1 audit fix: text-only max-output cutoff (openai-oauth HTTP/SSE
|
|
1334
|
-
// fallback maps status:'incomplete'/reason=max_output_tokens to
|
|
1335
|
-
// stopReason='length' above and treats it as success). Flag it so
|
|
1336
|
-
// loop.mjs can surface a truncation warning instead of accepting
|
|
1337
|
-
// silently-cut content as a clean final answer.
|
|
1338
|
-
...(stopReason === 'length' && content.length > 0 ? { truncated: true } : {}),
|
|
1339
|
-
responseId: responseId || undefined,
|
|
1340
|
-
serviceTier: serviceTier || undefined,
|
|
1341
|
-
};
|
|
1342
|
-
}
|
|
649
|
+
// --- HTTP/SSE fallback transport: extracted to openai-oauth-http-sse.mjs ---
|
|
1343
650
|
|
|
1344
651
|
// --- Provider ---
|
|
1345
652
|
export class OpenAIOAuthProvider {
|
|
@@ -1490,18 +797,12 @@ export class OpenAIOAuthProvider {
|
|
|
1490
797
|
const _authP = this.ensureAuth();
|
|
1491
798
|
let auth = await _authP;
|
|
1492
799
|
const body = await _bodyP;
|
|
1493
|
-
// poolKey
|
|
1494
|
-
// poolKey is per-session so parallel reviewer/worker callers each
|
|
1495
|
-
//
|
|
1496
|
-
//
|
|
1497
|
-
//
|
|
1498
|
-
//
|
|
1499
|
-
// `body.prompt_cache_key` and the handshake `session_id` header, so
|
|
1500
|
-
// compatible prefixes share cache without main/worker lanes evicting
|
|
1501
|
-
// each other.
|
|
1502
|
-
// poolKey defaults to sessionId (per-session socket isolation); cacheKey
|
|
1503
|
-
// never falls back to sessionId, so a fresh session still reuses the
|
|
1504
|
-
// warm prefix cache for the same route/prefix.
|
|
800
|
+
// poolKey != cacheKey by design (see openai-oauth-ws.mjs header note).
|
|
801
|
+
// poolKey is per-session so parallel reviewer/worker callers each get
|
|
802
|
+
// their own socket bucket. cacheKey is the Codex-style prompt_cache_key:
|
|
803
|
+
// by default it is the session/thread identity (clamped to 64 chars) and
|
|
804
|
+
// feeds both `body.prompt_cache_key` and the OAuth WS handshake
|
|
805
|
+
// `session_id`, so each long-lived thread keeps a stable cache shard.
|
|
1505
806
|
const poolKey = opts.sessionId || null;
|
|
1506
807
|
const cacheKey = body.prompt_cache_key || resolveProviderCacheKey(opts, 'openai-oauth');
|
|
1507
808
|
const iteration = Number.isFinite(Number(opts.iteration)) ? Number(opts.iteration) : null;
|
|
@@ -1717,7 +1018,7 @@ export class OpenAIOAuthProvider {
|
|
|
1717
1018
|
const items = Array.isArray(data?.models) ? data.models : [];
|
|
1718
1019
|
const normalized = items.map(m => _normalizeCodexModel(m));
|
|
1719
1020
|
_markLatestCodex(normalized);
|
|
1720
|
-
const enriched = await enrichModels(normalized);
|
|
1021
|
+
const enriched = sanitizeModelList((await enrichModels(normalized)).filter(Boolean), { provider: 'openai-oauth' });
|
|
1721
1022
|
await _saveCodexModelCache(enriched);
|
|
1722
1023
|
_lastCodexListModelsError = '';
|
|
1723
1024
|
return enriched;
|
|
@@ -1755,7 +1056,7 @@ export class OpenAIOAuthProvider {
|
|
|
1755
1056
|
const items = Array.isArray(data?.models) ? data.models : [];
|
|
1756
1057
|
const normalized = items.map(m => _normalizeCodexModel(m));
|
|
1757
1058
|
_markLatestCodex(normalized);
|
|
1758
|
-
const enriched = await enrichModels(normalized);
|
|
1059
|
+
const enriched = sanitizeModelList((await enrichModels(normalized)).filter(Boolean), { provider: 'openai-oauth' });
|
|
1759
1060
|
await _saveCodexModelCache(enriched);
|
|
1760
1061
|
if (!process.env.MIXDOG_QUIET_PROVIDER_LOG) process.stderr.write(`[openai-oauth] catalog refreshed (${enriched.length} models)\n`);
|
|
1761
1062
|
return enriched;
|
|
@@ -1774,168 +1075,12 @@ export class OpenAIOAuthProvider {
|
|
|
1774
1075
|
}
|
|
1775
1076
|
}
|
|
1776
1077
|
|
|
1777
|
-
|
|
1778
|
-
const
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
function generatePKCE() {
|
|
1787
|
-
const verifier = randomBytes(64).toString('base64url');
|
|
1788
|
-
const challenge = createHash('sha256').update(verifier).digest('base64url');
|
|
1789
|
-
return { verifier, challenge };
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1792
|
-
function _scrubOAuthLoginBody(text) {
|
|
1793
|
-
return String(text || '')
|
|
1794
|
-
.replace(/"access_token"\s*:\s*"[^"]+"/g, '"access_token":"[REDACTED]"')
|
|
1795
|
-
.replace(/"refresh_token"\s*:\s*"[^"]+"/g, '"refresh_token":"[REDACTED]"')
|
|
1796
|
-
.replace(/"id_token"\s*:\s*"[^"]+"/g, '"id_token":"[REDACTED]"')
|
|
1797
|
-
.replace(/[A-Za-z0-9_-]{32,}\.[A-Za-z0-9._-]+/g, '[REDACTED]');
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
|
-
function _parseOAuthCodeInput(input) {
|
|
1801
|
-
const value = String(input || '').trim();
|
|
1802
|
-
if (!value) return { code: '', state: '' };
|
|
1803
|
-
try {
|
|
1804
|
-
const url = new URL(value);
|
|
1805
|
-
const code = url.searchParams.get('code') || '';
|
|
1806
|
-
const state = url.searchParams.get('state') || '';
|
|
1807
|
-
if (code || state) return { code, state };
|
|
1808
|
-
} catch { /* not a URL */ }
|
|
1809
|
-
if (value.includes('#')) {
|
|
1810
|
-
const [code, state] = value.split('#', 2);
|
|
1811
|
-
return { code: String(code || '').trim(), state: String(state || '').trim() };
|
|
1812
|
-
}
|
|
1813
|
-
if (value.includes('code=')) {
|
|
1814
|
-
const params = new URLSearchParams(value.startsWith('?') ? value.slice(1) : value);
|
|
1815
|
-
return { code: params.get('code') || '', state: params.get('state') || '' };
|
|
1816
|
-
}
|
|
1817
|
-
return { code: value, state: '' };
|
|
1818
|
-
}
|
|
1819
|
-
|
|
1820
|
-
async function exchangeAuthorizationCode({ pkce, code }) {
|
|
1821
|
-
const cleanCode = String(code || '').trim();
|
|
1822
|
-
if (!cleanCode) throw new Error('[openai-oauth] authorization code is required');
|
|
1823
|
-
const tokenRes = await fetch(TOKEN_URL, {
|
|
1824
|
-
method: 'POST',
|
|
1825
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
1826
|
-
body: new URLSearchParams({
|
|
1827
|
-
grant_type: 'authorization_code',
|
|
1828
|
-
code: cleanCode,
|
|
1829
|
-
redirect_uri: REDIRECT_URI,
|
|
1830
|
-
client_id: CLIENT_ID,
|
|
1831
|
-
code_verifier: pkce.verifier,
|
|
1832
|
-
}),
|
|
1833
|
-
redirect: 'error',
|
|
1834
|
-
signal: AbortSignal.timeout(TOKEN_TIMEOUT_MS),
|
|
1835
|
-
});
|
|
1836
|
-
if (!tokenRes.ok) {
|
|
1837
|
-
const text = await tokenRes.text().catch(() => '');
|
|
1838
|
-
throw new Error(`[openai-oauth] token exchange ${tokenRes.status}: ${_scrubOAuthLoginBody(text).slice(0, 500)}`);
|
|
1839
|
-
}
|
|
1840
|
-
const json = await tokenRes.json();
|
|
1841
|
-
if (!json.access_token || !json.refresh_token) {
|
|
1842
|
-
throw new Error('[openai-oauth] token exchange response missing access_token or refresh_token');
|
|
1843
|
-
}
|
|
1844
|
-
const expiresAt = (typeof json.expires_in === 'number'
|
|
1845
|
-
? Date.now() + json.expires_in * 1000
|
|
1846
|
-
: 0) || _expiryFromAccessToken(json.access_token);
|
|
1847
|
-
const tokens = {
|
|
1848
|
-
access_token: json.access_token,
|
|
1849
|
-
refresh_token: json.refresh_token,
|
|
1850
|
-
expires_at: expiresAt,
|
|
1851
|
-
account_id: extractAccountId(json.access_token),
|
|
1852
|
-
};
|
|
1853
|
-
saveTokens(tokens);
|
|
1854
|
-
return tokens;
|
|
1855
|
-
}
|
|
1856
|
-
|
|
1857
|
-
export async function beginOAuthLogin() {
|
|
1858
|
-
const pkce = generatePKCE();
|
|
1859
|
-
const state = randomBytes(16).toString('hex');
|
|
1860
|
-
const url = new URL(AUTHORIZE_URL);
|
|
1861
|
-
url.searchParams.set('response_type', 'code');
|
|
1862
|
-
url.searchParams.set('client_id', CLIENT_ID);
|
|
1863
|
-
url.searchParams.set('redirect_uri', REDIRECT_URI);
|
|
1864
|
-
url.searchParams.set('scope', CODEX_OAUTH_SCOPE);
|
|
1865
|
-
url.searchParams.set('code_challenge', pkce.challenge);
|
|
1866
|
-
url.searchParams.set('code_challenge_method', 'S256');
|
|
1867
|
-
url.searchParams.set('id_token_add_organizations', 'true');
|
|
1868
|
-
url.searchParams.set('codex_cli_simplified_flow', 'true');
|
|
1869
|
-
url.searchParams.set('state', state);
|
|
1870
|
-
url.searchParams.set('originator', CODEX_OAUTH_ORIGINATOR);
|
|
1871
|
-
|
|
1872
|
-
let server = null;
|
|
1873
|
-
let timeout = null;
|
|
1874
|
-
let finish = null;
|
|
1875
|
-
const waitForCallback = new Promise((resolve, reject) => {
|
|
1876
|
-
let settled = false;
|
|
1877
|
-
finish = (value, error = null) => {
|
|
1878
|
-
if (settled) return;
|
|
1879
|
-
settled = true;
|
|
1880
|
-
if (timeout) clearTimeout(timeout);
|
|
1881
|
-
try { server?.close(); } catch { /* already closed */ }
|
|
1882
|
-
if (error) reject(error);
|
|
1883
|
-
else resolve(value);
|
|
1884
|
-
};
|
|
1885
|
-
server = createServer(async (req, res) => {
|
|
1886
|
-
const u = new URL(req.url || '/', `http://${CALLBACK_HOST}:${CALLBACK_PORT}`);
|
|
1887
|
-
if (u.pathname !== CALLBACK_PATH) {
|
|
1888
|
-
res.writeHead(404);
|
|
1889
|
-
res.end();
|
|
1890
|
-
return;
|
|
1891
|
-
}
|
|
1892
|
-
const code = u.searchParams.get('code');
|
|
1893
|
-
if (!code || u.searchParams.get('state') !== state) {
|
|
1894
|
-
res.writeHead(400);
|
|
1895
|
-
res.end('Invalid');
|
|
1896
|
-
finish(null);
|
|
1897
|
-
return;
|
|
1898
|
-
}
|
|
1899
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1900
|
-
res.end('<html><body><h2>OpenAI OAuth login successful! You can close this tab.</h2></body></html>');
|
|
1901
|
-
try {
|
|
1902
|
-
const tokens = await exchangeAuthorizationCode({ pkce, code });
|
|
1903
|
-
finish(tokens);
|
|
1904
|
-
} catch (err) {
|
|
1905
|
-
finish(null, err instanceof Error ? err : new Error(String(err)));
|
|
1906
|
-
}
|
|
1907
|
-
});
|
|
1908
|
-
timeout = setTimeout(() => finish(null), LOGIN_TIMEOUT_MS);
|
|
1909
|
-
server.listen(CALLBACK_PORT, CALLBACK_HOST, async () => {
|
|
1910
|
-
process.stderr.write(`\n[openai-oauth] Open this URL to log in to ChatGPT (OpenAI OAuth):\n${url.toString()}\n\n`);
|
|
1911
|
-
try {
|
|
1912
|
-
const { openInBrowser } = await import('../../../shared/open-url.mjs');
|
|
1913
|
-
openInBrowser(url.toString());
|
|
1914
|
-
} catch (err) {
|
|
1915
|
-
process.stderr.write(`[openai-oauth] browser open failed: ${String(err?.message || err).slice(0, 200)}\n`);
|
|
1916
|
-
}
|
|
1917
|
-
});
|
|
1918
|
-
server.on('error', (err) => finish(null, new Error(`[openai-oauth] callback server failed on ${CALLBACK_HOST}:${CALLBACK_PORT}: ${err?.message || err}`)));
|
|
1919
|
-
});
|
|
1920
|
-
|
|
1921
|
-
return {
|
|
1922
|
-
provider: 'openai-oauth',
|
|
1923
|
-
url: url.toString(),
|
|
1924
|
-
waitForCallback,
|
|
1925
|
-
completeCode: async (input) => {
|
|
1926
|
-
const parsed = _parseOAuthCodeInput(input);
|
|
1927
|
-
if (parsed.state && parsed.state !== state) throw new Error('[openai-oauth] OAuth state mismatch');
|
|
1928
|
-
const tokens = await exchangeAuthorizationCode({ pkce, code: parsed.code });
|
|
1929
|
-
finish?.(tokens);
|
|
1930
|
-
return tokens;
|
|
1931
|
-
},
|
|
1932
|
-
cancel: () => {
|
|
1933
|
-
finish?.(null);
|
|
1934
|
-
},
|
|
1935
|
-
};
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
|
-
export async function loginOAuth() {
|
|
1939
|
-
const login = await beginOAuthLogin();
|
|
1940
|
-
return await login.waitForCallback;
|
|
1941
|
-
}
|
|
1078
|
+
// --- OAuth PKCE login flow: extracted to openai-oauth-login.mjs ---
|
|
1079
|
+
const { beginOAuthLogin, loginOAuth } = createOpenAIOAuthLogin({
|
|
1080
|
+
clientId: CLIENT_ID,
|
|
1081
|
+
originator: CODEX_OAUTH_ORIGINATOR,
|
|
1082
|
+
extractAccountId,
|
|
1083
|
+
expiryFromAccessToken: _expiryFromAccessToken,
|
|
1084
|
+
saveTokens,
|
|
1085
|
+
});
|
|
1086
|
+
export { beginOAuthLogin, loginOAuth };
|