@xyit/wellspring 0.0.0-stage → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +853 -2
- package/dist/agent/pending-asks.d.ts +20 -0
- package/dist/agent/pending-asks.js +70 -0
- package/dist/agent/pending-asks.js.map +1 -0
- package/dist/agent/task-registry.d.ts +14 -0
- package/dist/agent/task-registry.js +80 -0
- package/dist/agent/task-registry.js.map +1 -0
- package/dist/agent/tool-context.d.ts +30 -0
- package/dist/agent/tool-context.js +10 -0
- package/dist/agent/tool-context.js.map +1 -0
- package/dist/agent/usage.d.ts +40 -0
- package/dist/agent/usage.js +122 -0
- package/dist/agent/usage.js.map +1 -0
- package/dist/api/host/agent.d.ts +165 -0
- package/dist/api/host/agent.js +5 -0
- package/dist/api/host/agent.js.map +1 -0
- package/dist/api/host/events.d.ts +2 -0
- package/dist/api/host/events.js +6 -0
- package/dist/api/host/events.js.map +1 -0
- package/dist/api/host/exec.d.ts +36 -0
- package/dist/api/host/exec.js +10 -0
- package/dist/api/host/exec.js.map +1 -0
- package/dist/api/host/fs.d.ts +24 -0
- package/dist/api/host/fs.js +7 -0
- package/dist/api/host/fs.js.map +1 -0
- package/dist/api/host/hooks.d.ts +210 -0
- package/dist/api/host/hooks.js +401 -0
- package/dist/api/host/hooks.js.map +1 -0
- package/dist/api/host/host.d.ts +36 -0
- package/dist/api/host/host.js +53 -0
- package/dist/api/host/host.js.map +1 -0
- package/dist/api/host/http.d.ts +58 -0
- package/dist/api/host/http.js +6 -0
- package/dist/api/host/http.js.map +1 -0
- package/dist/api/host/index.d.ts +15 -0
- package/dist/api/host/index.js +20 -0
- package/dist/api/host/index.js.map +1 -0
- package/dist/api/host/models.d.ts +65 -0
- package/dist/api/host/models.js +8 -0
- package/dist/api/host/models.js.map +1 -0
- package/dist/api/host/network.d.ts +22 -0
- package/dist/api/host/network.js +7 -0
- package/dist/api/host/network.js.map +1 -0
- package/dist/api/host/plugin.d.ts +138 -0
- package/dist/api/host/plugin.js +6 -0
- package/dist/api/host/plugin.js.map +1 -0
- package/dist/api/host/plugins.d.ts +49 -0
- package/dist/api/host/plugins.js +6 -0
- package/dist/api/host/plugins.js.map +1 -0
- package/dist/api/host/pty.d.ts +55 -0
- package/dist/api/host/pty.js +21 -0
- package/dist/api/host/pty.js.map +1 -0
- package/dist/api/host/session.d.ts +5 -0
- package/dist/api/host/session.js +52 -0
- package/dist/api/host/session.js.map +1 -0
- package/dist/api/host/version.d.ts +2 -0
- package/dist/api/host/version.js +3 -0
- package/dist/api/host/version.js.map +1 -0
- package/dist/api/host/workspace.d.ts +38 -0
- package/dist/api/host/workspace.js +9 -0
- package/dist/api/host/workspace.js.map +1 -0
- package/dist/api/plugin/http-client.d.ts +42 -0
- package/dist/api/plugin/http-client.js +364 -0
- package/dist/api/plugin/http-client.js.map +1 -0
- package/dist/api/plugin/http-protocol.d.ts +152 -0
- package/dist/api/plugin/http-protocol.js +49 -0
- package/dist/api/plugin/http-protocol.js.map +1 -0
- package/dist/api/result/result.d.ts +16 -0
- package/dist/api/result/result.js +43 -0
- package/dist/api/result/result.js.map +1 -0
- package/dist/commands/agent-turn.d.ts +63 -0
- package/dist/commands/agent-turn.js +687 -0
- package/dist/commands/agent-turn.js.map +1 -0
- package/dist/commands/ask-prompt.d.ts +6 -0
- package/dist/commands/ask-prompt.js +49 -0
- package/dist/commands/ask-prompt.js.map +1 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +283 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/conversation.d.ts +51 -0
- package/dist/commands/conversation.js +630 -0
- package/dist/commands/conversation.js.map +1 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +77 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/exec-approval.d.ts +3 -0
- package/dist/commands/exec-approval.js +80 -0
- package/dist/commands/exec-approval.js.map +1 -0
- package/dist/commands/help.d.ts +2 -0
- package/dist/commands/help.js +35 -0
- package/dist/commands/help.js.map +1 -0
- package/dist/commands/key.d.ts +2 -0
- package/dist/commands/key.js +226 -0
- package/dist/commands/key.js.map +1 -0
- package/dist/commands/plugin.d.ts +2 -0
- package/dist/commands/plugin.js +190 -0
- package/dist/commands/plugin.js.map +1 -0
- package/dist/commands/session.d.ts +2 -0
- package/dist/commands/session.js +167 -0
- package/dist/commands/session.js.map +1 -0
- package/dist/commands/shared.d.ts +35 -0
- package/dist/commands/shared.js +66 -0
- package/dist/commands/shared.js.map +1 -0
- package/dist/commands/start.d.ts +2 -0
- package/dist/commands/start.js +75 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/stop.d.ts +2 -0
- package/dist/commands/stop.js +24 -0
- package/dist/commands/stop.js.map +1 -0
- package/dist/commands/version.d.ts +2 -0
- package/dist/commands/version.js +33 -0
- package/dist/commands/version.js.map +1 -0
- package/dist/config/codes.d.ts +13 -0
- package/dist/config/codes.js +15 -0
- package/dist/config/codes.js.map +1 -0
- package/dist/config/credentials.d.ts +67 -0
- package/dist/config/credentials.js +214 -0
- package/dist/config/credentials.js.map +1 -0
- package/dist/config/defaults.d.ts +5 -0
- package/dist/config/defaults.js +31 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/index.d.ts +5 -0
- package/dist/config/index.js +10 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/loader.d.ts +10 -0
- package/dist/config/loader.js +68 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/providers.d.ts +30 -0
- package/dist/config/providers.js +97 -0
- package/dist/config/providers.js.map +1 -0
- package/dist/config/types.d.ts +75 -0
- package/dist/config/types.js +4 -0
- package/dist/config/types.js.map +1 -0
- package/dist/config/validate.d.ts +4 -0
- package/dist/config/validate.js +391 -0
- package/dist/config/validate.js.map +1 -0
- package/dist/display/markdown.d.ts +7 -0
- package/dist/display/markdown.js +116 -0
- package/dist/display/markdown.js.map +1 -0
- package/dist/display/tool-label.d.ts +6 -0
- package/dist/display/tool-label.js +81 -0
- package/dist/display/tool-label.js.map +1 -0
- package/dist/event/event.d.ts +27 -0
- package/dist/event/event.js +152 -0
- package/dist/event/event.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +308 -0
- package/dist/index.js.map +1 -0
- package/dist/input/codes.d.ts +15 -0
- package/dist/input/codes.js +20 -0
- package/dist/input/codes.js.map +1 -0
- package/dist/input/commands.d.ts +10 -0
- package/dist/input/commands.js +65 -0
- package/dist/input/commands.js.map +1 -0
- package/dist/input/help.d.ts +8 -0
- package/dist/input/help.js +51 -0
- package/dist/input/help.js.map +1 -0
- package/dist/input/index.d.ts +6 -0
- package/dist/input/index.js +10 -0
- package/dist/input/index.js.map +1 -0
- package/dist/input/parse-options.d.ts +11 -0
- package/dist/input/parse-options.js +51 -0
- package/dist/input/parse-options.js.map +1 -0
- package/dist/input/parse.d.ts +3 -0
- package/dist/input/parse.js +119 -0
- package/dist/input/parse.js.map +1 -0
- package/dist/input/types.d.ts +17 -0
- package/dist/input/types.js +2 -0
- package/dist/input/types.js.map +1 -0
- package/dist/logger/elog4js.d.ts +65 -0
- package/dist/logger/elog4js.js +305 -0
- package/dist/logger/elog4js.js.map +1 -0
- package/dist/logger/logger.d.ts +37 -0
- package/dist/logger/logger.js +196 -0
- package/dist/logger/logger.js.map +1 -0
- package/dist/network/index.d.ts +2 -0
- package/dist/network/index.js +6 -0
- package/dist/network/index.js.map +1 -0
- package/dist/network/openai.d.ts +117 -0
- package/dist/network/openai.js +443 -0
- package/dist/network/openai.js.map +1 -0
- package/dist/network/proxy.d.ts +9 -0
- package/dist/network/proxy.js +72 -0
- package/dist/network/proxy.js.map +1 -0
- package/dist/network/web.d.ts +34 -0
- package/dist/network/web.js +152 -0
- package/dist/network/web.js.map +1 -0
- package/dist/platform/clipboard.d.ts +15 -0
- package/dist/platform/clipboard.js +82 -0
- package/dist/platform/clipboard.js.map +1 -0
- package/dist/platform/exec.d.ts +28 -0
- package/dist/platform/exec.js +95 -0
- package/dist/platform/exec.js.map +1 -0
- package/dist/platform/file.d.ts +38 -0
- package/dist/platform/file.js +197 -0
- package/dist/platform/file.js.map +1 -0
- package/dist/platform/index.d.ts +7 -0
- package/dist/platform/index.js +12 -0
- package/dist/platform/index.js.map +1 -0
- package/dist/platform/notification.d.ts +14 -0
- package/dist/platform/notification.js +82 -0
- package/dist/platform/notification.js.map +1 -0
- package/dist/platform/open-external.d.ts +10 -0
- package/dist/platform/open-external.js +105 -0
- package/dist/platform/open-external.js.map +1 -0
- package/dist/platform/os.d.ts +20 -0
- package/dist/platform/os.js +86 -0
- package/dist/platform/os.js.map +1 -0
- package/dist/platform/path.d.ts +32 -0
- package/dist/platform/path.js +154 -0
- package/dist/platform/path.js.map +1 -0
- package/dist/plugin/access-guard.d.ts +1 -0
- package/dist/plugin/access-guard.js +40 -0
- package/dist/plugin/access-guard.js.map +1 -0
- package/dist/plugin/agent-service.d.ts +25 -0
- package/dist/plugin/agent-service.js +809 -0
- package/dist/plugin/agent-service.js.map +1 -0
- package/dist/plugin/agent-tools.d.ts +12 -0
- package/dist/plugin/agent-tools.js +31 -0
- package/dist/plugin/agent-tools.js.map +1 -0
- package/dist/plugin/capabilities.d.ts +26 -0
- package/dist/plugin/capabilities.js +192 -0
- package/dist/plugin/capabilities.js.map +1 -0
- package/dist/plugin/console-guard.d.ts +2 -0
- package/dist/plugin/console-guard.js +52 -0
- package/dist/plugin/console-guard.js.map +1 -0
- package/dist/plugin/data-access.d.ts +15 -0
- package/dist/plugin/data-access.js +57 -0
- package/dist/plugin/data-access.js.map +1 -0
- package/dist/plugin/fs.d.ts +3 -0
- package/dist/plugin/fs.js +93 -0
- package/dist/plugin/fs.js.map +1 -0
- package/dist/plugin/host-alias.d.ts +2 -0
- package/dist/plugin/host-alias.js +57 -0
- package/dist/plugin/host-alias.js.map +1 -0
- package/dist/plugin/index.d.ts +7 -0
- package/dist/plugin/index.js +11 -0
- package/dist/plugin/index.js.map +1 -0
- package/dist/plugin/install.d.ts +17 -0
- package/dist/plugin/install.js +163 -0
- package/dist/plugin/install.js.map +1 -0
- package/dist/plugin/loader.d.ts +45 -0
- package/dist/plugin/loader.js +739 -0
- package/dist/plugin/loader.js.map +1 -0
- package/dist/plugin/manage.d.ts +31 -0
- package/dist/plugin/manage.js +271 -0
- package/dist/plugin/manage.js.map +1 -0
- package/dist/plugin/models-service.d.ts +15 -0
- package/dist/plugin/models-service.js +317 -0
- package/dist/plugin/models-service.js.map +1 -0
- package/dist/plugin/network-service.d.ts +10 -0
- package/dist/plugin/network-service.js +87 -0
- package/dist/plugin/network-service.js.map +1 -0
- package/dist/plugin/package-manager.d.ts +46 -0
- package/dist/plugin/package-manager.js +148 -0
- package/dist/plugin/package-manager.js.map +1 -0
- package/dist/plugin/paths.d.ts +6 -0
- package/dist/plugin/paths.js +15 -0
- package/dist/plugin/paths.js.map +1 -0
- package/dist/plugin/registry.d.ts +17 -0
- package/dist/plugin/registry.js +82 -0
- package/dist/plugin/registry.js.map +1 -0
- package/dist/plugin/runtime.d.ts +9 -0
- package/dist/plugin/runtime.js +43 -0
- package/dist/plugin/runtime.js.map +1 -0
- package/dist/plugin/scaffold.d.ts +13 -0
- package/dist/plugin/scaffold.js +132 -0
- package/dist/plugin/scaffold.js.map +1 -0
- package/dist/plugin/state.d.ts +6 -0
- package/dist/plugin/state.js +38 -0
- package/dist/plugin/state.js.map +1 -0
- package/dist/plugin/zip.d.ts +7 -0
- package/dist/plugin/zip.js +138 -0
- package/dist/plugin/zip.js.map +1 -0
- package/dist/sandbox/audit.d.ts +12 -0
- package/dist/sandbox/audit.js +20 -0
- package/dist/sandbox/audit.js.map +1 -0
- package/dist/sandbox/backends/bwrap.d.ts +28 -0
- package/dist/sandbox/backends/bwrap.js +197 -0
- package/dist/sandbox/backends/bwrap.js.map +1 -0
- package/dist/sandbox/backends/direct.d.ts +7 -0
- package/dist/sandbox/backends/direct.js +32 -0
- package/dist/sandbox/backends/direct.js.map +1 -0
- package/dist/sandbox/bridge-state.d.ts +13 -0
- package/dist/sandbox/bridge-state.js +12 -0
- package/dist/sandbox/bridge-state.js.map +1 -0
- package/dist/sandbox/index.d.ts +5 -0
- package/dist/sandbox/index.js +6 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/sandbox/manager.d.ts +8 -0
- package/dist/sandbox/manager.js +325 -0
- package/dist/sandbox/manager.js.map +1 -0
- package/dist/sandbox/memory-limit.d.ts +44 -0
- package/dist/sandbox/memory-limit.js +193 -0
- package/dist/sandbox/memory-limit.js.map +1 -0
- package/dist/sandbox/plugin-client.d.ts +80 -0
- package/dist/sandbox/plugin-client.js +1601 -0
- package/dist/sandbox/plugin-client.js.map +1 -0
- package/dist/sandbox/plugin-host.d.ts +1 -0
- package/dist/sandbox/plugin-host.js +1001 -0
- package/dist/sandbox/plugin-host.js.map +1 -0
- package/dist/sandbox/protocol.d.ts +43 -0
- package/dist/sandbox/protocol.js +96 -0
- package/dist/sandbox/protocol.js.map +1 -0
- package/dist/sandbox/rpc-timeouts.d.ts +28 -0
- package/dist/sandbox/rpc-timeouts.js +15 -0
- package/dist/sandbox/rpc-timeouts.js.map +1 -0
- package/dist/sandbox/supervisor.d.ts +31 -0
- package/dist/sandbox/supervisor.js +114 -0
- package/dist/sandbox/supervisor.js.map +1 -0
- package/dist/sandbox/types.d.ts +124 -0
- package/dist/sandbox/types.js +17 -0
- package/dist/sandbox/types.js.map +1 -0
- package/dist/service/control.d.ts +32 -0
- package/dist/service/control.js +117 -0
- package/dist/service/control.js.map +1 -0
- package/dist/session/compress.d.ts +3 -0
- package/dist/session/compress.js +42 -0
- package/dist/session/compress.js.map +1 -0
- package/dist/session/index.d.ts +4 -0
- package/dist/session/index.js +8 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/registry.d.ts +4 -0
- package/dist/session/registry.js +12 -0
- package/dist/session/registry.js.map +1 -0
- package/dist/session/replay.d.ts +10 -0
- package/dist/session/replay.js +109 -0
- package/dist/session/replay.js.map +1 -0
- package/dist/session/session.d.ts +40 -0
- package/dist/session/session.js +108 -0
- package/dist/session/session.js.map +1 -0
- package/dist/session/store.d.ts +58 -0
- package/dist/session/store.js +382 -0
- package/dist/session/store.js.map +1 -0
- package/dist/session/types.d.ts +56 -0
- package/dist/session/types.js +3 -0
- package/dist/session/types.js.map +1 -0
- package/dist/workspace/index.d.ts +7 -0
- package/dist/workspace/index.js +17 -0
- package/dist/workspace/index.js.map +1 -0
- package/dist/workspace/registry.d.ts +25 -0
- package/dist/workspace/registry.js +293 -0
- package/dist/workspace/registry.js.map +1 -0
- package/package.json +73 -5
- package/scripts/build-node-pty.mjs +55 -0
- package/scripts/rewrite-dts.mjs +36 -0
- package/scripts/run-tests.mjs +41 -0
- package/scripts/verify-sandbox.ts +98 -0
- package/src/agent/pending-asks.ts +88 -0
- package/src/agent/task-registry.ts +108 -0
- package/src/agent/tool-context.ts +47 -0
- package/src/agent/usage.ts +170 -0
- package/src/api/host/agent.ts +162 -0
- package/src/api/host/events.ts +7 -0
- package/src/api/host/exec.ts +47 -0
- package/src/api/host/fs.ts +33 -0
- package/src/api/host/hooks.ts +635 -0
- package/src/api/host/host.ts +99 -0
- package/src/api/host/http.ts +79 -0
- package/src/api/host/index.ts +20 -0
- package/src/api/host/models.ts +81 -0
- package/src/api/host/network.ts +32 -0
- package/src/api/host/plugin.ts +158 -0
- package/src/api/host/plugins.ts +59 -0
- package/src/api/host/pty.ts +73 -0
- package/src/api/host/session.ts +61 -0
- package/src/api/host/version.ts +2 -0
- package/src/api/host/workspace.ts +51 -0
- package/src/api/plugin/http-client.ts +492 -0
- package/src/api/plugin/http-protocol.ts +231 -0
- package/src/api/result/result.ts +52 -0
- package/src/commands/agent-turn.ts +817 -0
- package/src/commands/ask-prompt.ts +52 -0
- package/src/commands/config.ts +306 -0
- package/src/commands/conversation.ts +777 -0
- package/src/commands/doctor.ts +92 -0
- package/src/commands/exec-approval.ts +91 -0
- package/src/commands/help.ts +41 -0
- package/src/commands/key.ts +281 -0
- package/src/commands/plugin.ts +222 -0
- package/src/commands/session.ts +179 -0
- package/src/commands/shared.ts +103 -0
- package/src/commands/start.ts +88 -0
- package/src/commands/stop.ts +24 -0
- package/src/commands/version.ts +40 -0
- package/src/config/codes.ts +17 -0
- package/src/config/credentials.ts +302 -0
- package/src/config/defaults.ts +32 -0
- package/src/config/index.ts +10 -0
- package/src/config/loader.ts +80 -0
- package/src/config/providers.ts +133 -0
- package/src/config/types.ts +88 -0
- package/src/config/validate.ts +458 -0
- package/src/display/markdown.ts +137 -0
- package/src/display/tool-label.ts +87 -0
- package/src/event/event.ts +171 -0
- package/src/index.ts +344 -0
- package/src/input/codes.ts +21 -0
- package/src/input/commands.ts +76 -0
- package/src/input/help.ts +60 -0
- package/src/input/index.ts +10 -0
- package/src/input/parse-options.ts +66 -0
- package/src/input/parse.ts +138 -0
- package/src/input/types.ts +19 -0
- package/src/logger/elog4js.ts +366 -0
- package/src/logger/logger.ts +251 -0
- package/src/network/index.ts +6 -0
- package/src/network/openai.ts +571 -0
- package/src/network/proxy.ts +80 -0
- package/src/network/web.ts +206 -0
- package/src/platform/clipboard.ts +107 -0
- package/src/platform/exec.ts +128 -0
- package/src/platform/file.ts +235 -0
- package/src/platform/index.ts +12 -0
- package/src/platform/notification.ts +108 -0
- package/src/platform/open-external.ts +132 -0
- package/src/platform/os.ts +101 -0
- package/src/platform/path.ts +214 -0
- package/src/plugin/access-guard.ts +46 -0
- package/src/plugin/agent-service.ts +969 -0
- package/src/plugin/agent-tools.ts +43 -0
- package/src/plugin/capabilities.ts +240 -0
- package/src/plugin/console-guard.ts +66 -0
- package/src/plugin/data-access.ts +70 -0
- package/src/plugin/fs.ts +109 -0
- package/src/plugin/host-alias.ts +61 -0
- package/src/plugin/index.ts +11 -0
- package/src/plugin/install.ts +213 -0
- package/src/plugin/loader.ts +952 -0
- package/src/plugin/manage.ts +338 -0
- package/src/plugin/models-service.ts +383 -0
- package/src/plugin/network-service.ts +102 -0
- package/src/plugin/package-manager.ts +214 -0
- package/src/plugin/paths.ts +19 -0
- package/src/plugin/registry.ts +103 -0
- package/src/plugin/runtime.ts +57 -0
- package/src/plugin/scaffold.ts +158 -0
- package/src/plugin/state.ts +52 -0
- package/src/plugin/zip.ts +156 -0
- package/src/sandbox/audit.ts +38 -0
- package/src/sandbox/backends/bwrap.ts +251 -0
- package/src/sandbox/backends/direct.ts +43 -0
- package/src/sandbox/bridge-state.ts +29 -0
- package/src/sandbox/index.ts +5 -0
- package/src/sandbox/manager.ts +406 -0
- package/src/sandbox/memory-limit.ts +245 -0
- package/src/sandbox/plugin-client.ts +1881 -0
- package/src/sandbox/plugin-host.ts +1282 -0
- package/src/sandbox/protocol.ts +142 -0
- package/src/sandbox/pty-bridge.py +154 -0
- package/src/sandbox/rpc-timeouts.ts +43 -0
- package/src/sandbox/supervisor.ts +172 -0
- package/src/sandbox/types.ts +149 -0
- package/src/service/control.ts +155 -0
- package/src/session/compress.ts +47 -0
- package/src/session/index.ts +8 -0
- package/src/session/registry.ts +17 -0
- package/src/session/replay.ts +132 -0
- package/src/session/session.ts +134 -0
- package/src/session/store.ts +468 -0
- package/src/session/types.ts +61 -0
- package/src/workspace/index.ts +21 -0
- package/src/workspace/registry.ts +356 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* OpenAI SDK 接入。
|
|
3
|
+
* Responses API 为默认文本接口;chat 兼容只提供 Chat Completions 的网关。
|
|
4
|
+
* API key 读取顺序:显式参数 > OPENAI_API_KEY > <configDir>/credentials.json,
|
|
5
|
+
* 明文只进请求,不进配置、日志与界面。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import OpenAI, {
|
|
9
|
+
APIConnectionError,
|
|
10
|
+
APIConnectionTimeoutError,
|
|
11
|
+
APIError,
|
|
12
|
+
APIUserAbortError,
|
|
13
|
+
AuthenticationError,
|
|
14
|
+
BadRequestError,
|
|
15
|
+
InternalServerError,
|
|
16
|
+
NotFoundError,
|
|
17
|
+
PermissionDeniedError,
|
|
18
|
+
RateLimitError,
|
|
19
|
+
type ClientOptions,
|
|
20
|
+
} from "openai"
|
|
21
|
+
import type {
|
|
22
|
+
ChatCompletionMessageParam,
|
|
23
|
+
ChatCompletionTool,
|
|
24
|
+
} from "openai/resources/chat/completions"
|
|
25
|
+
import type { Logger } from "pino"
|
|
26
|
+
|
|
27
|
+
import { parseProviderUsage, type ProviderUsage } from "../agent/usage.ts"
|
|
28
|
+
import { Result } from "../api/result/result.ts"
|
|
29
|
+
import { normalizeApiKey, resolveApiKeySync } from "../config/credentials.ts"
|
|
30
|
+
import { DEFAULT_SETTINGS } from "../config/defaults.ts"
|
|
31
|
+
import type { OpenAISetting } from "../config/types.ts"
|
|
32
|
+
import { readEnv } from "../platform/os.ts"
|
|
33
|
+
|
|
34
|
+
/** OpenAI 调用错误码 */
|
|
35
|
+
export const OPENAI_CODES = {
|
|
36
|
+
missingApiKey: "openai.missing-api-key",
|
|
37
|
+
clientInitFailed: "openai.client-init-failed",
|
|
38
|
+
emptyOutput: "openai.empty-output",
|
|
39
|
+
timeout: "openai.timeout",
|
|
40
|
+
connectionFailed: "openai.connection-failed",
|
|
41
|
+
aborted: "openai.aborted",
|
|
42
|
+
unauthorized: "openai.unauthorized",
|
|
43
|
+
forbidden: "openai.forbidden",
|
|
44
|
+
notFound: "openai.not-found",
|
|
45
|
+
rateLimited: "openai.rate-limited",
|
|
46
|
+
badRequest: "openai.bad-request",
|
|
47
|
+
serverError: "openai.server-error",
|
|
48
|
+
apiError: "openai.api-error",
|
|
49
|
+
requestFailed: "openai.request-failed",
|
|
50
|
+
} as const
|
|
51
|
+
|
|
52
|
+
export interface OpenAIReply {
|
|
53
|
+
readonly text: string
|
|
54
|
+
readonly model: string
|
|
55
|
+
readonly id: string
|
|
56
|
+
readonly inputTokens: number | null
|
|
57
|
+
readonly outputTokens: number | null
|
|
58
|
+
/** 缓存命中 / 写入的 prompt tokens;供应商没报告时缺省 */
|
|
59
|
+
readonly cacheReadTokens?: number | null
|
|
60
|
+
readonly cacheWriteTokens?: number | null
|
|
61
|
+
/** 输出里的推理 token;供应商没报告时缺省 */
|
|
62
|
+
readonly reasoningTokens?: number | null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 把供应商 usage 里的缓存/推理项展开成可选字段,空值不写进回复对象 */
|
|
66
|
+
function providerUsageFields(
|
|
67
|
+
usage: ProviderUsage | null,
|
|
68
|
+
): Pick<OpenAIReply, "cacheReadTokens" | "cacheWriteTokens" | "reasoningTokens"> {
|
|
69
|
+
if (usage === null) {
|
|
70
|
+
return {}
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
...(usage.cacheReadTokens === null ? {} : { cacheReadTokens: usage.cacheReadTokens }),
|
|
74
|
+
...(usage.cacheWriteTokens === null ? {} : { cacheWriteTokens: usage.cacheWriteTokens }),
|
|
75
|
+
...(usage.reasoningTokens === null ? {} : { reasoningTokens: usage.reasoningTokens }),
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface RespondOptions {
|
|
80
|
+
readonly model?: string
|
|
81
|
+
readonly instructions?: string
|
|
82
|
+
readonly maxOutputTokens?: number
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ChatOptions {
|
|
86
|
+
readonly model?: string
|
|
87
|
+
readonly temperature?: number
|
|
88
|
+
readonly maxTokens?: number
|
|
89
|
+
/** 推理强度;off 表示不发送 reasoning 参数 */
|
|
90
|
+
readonly reasoning?: string
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 规范化后的流式片段,直接落进会话事件 */
|
|
94
|
+
export interface OpenAISessionChunk {
|
|
95
|
+
readonly type: "chunk"
|
|
96
|
+
readonly time: number
|
|
97
|
+
readonly chunk: Record<string, unknown>
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface ToolCall {
|
|
101
|
+
readonly id: string
|
|
102
|
+
readonly name: string
|
|
103
|
+
readonly arguments: string
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface StreamReply {
|
|
107
|
+
/** 这次流被 signal 中断(用户停止);reply 里是已经收到的部分 */
|
|
108
|
+
readonly interrupted?: boolean
|
|
109
|
+
readonly reply: OpenAIReply
|
|
110
|
+
readonly chunks: readonly OpenAISessionChunk[]
|
|
111
|
+
readonly toolCalls: readonly ToolCall[]
|
|
112
|
+
/** 模型返回的思考内容(DeepSeek 等推理模型的 reasoning_content);没有时为空串 */
|
|
113
|
+
readonly reasoning: string
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface ChatStreamOptions extends ChatOptions {
|
|
117
|
+
/**
|
|
118
|
+
* 中断信号:用户停止时立刻断开这次请求
|
|
119
|
+
*
|
|
120
|
+
* 已经收到的增量照常返回(`interrupted: true`),调用方按「用户停止」收尾,
|
|
121
|
+
* 不会因为打断丢掉半截回复。
|
|
122
|
+
*/
|
|
123
|
+
readonly signal?: AbortSignal
|
|
124
|
+
readonly onText?: (text: string) => void
|
|
125
|
+
/** 思考内容增量回调;只在模型返回 reasoning_content 时触发 */
|
|
126
|
+
readonly onReasoning?: (text: string) => void
|
|
127
|
+
readonly tools?: readonly ChatCompletionTool[]
|
|
128
|
+
readonly toolChoice?: "auto" | "none" | "required"
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface CreateOpenAIClientOptions {
|
|
132
|
+
readonly settings?: OpenAISetting
|
|
133
|
+
/** 覆盖 OPENAI_API_KEY 与本地凭据 */
|
|
134
|
+
readonly apiKey?: string
|
|
135
|
+
/** 本地凭据文件路径;不传用当前 configDir 下的 credentials.json */
|
|
136
|
+
readonly credentialsFile?: string
|
|
137
|
+
readonly logger?: Logger
|
|
138
|
+
/** 注入 fetch,测试用 */
|
|
139
|
+
readonly fetch?: ClientOptions["fetch"]
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 推理强度映射:
|
|
144
|
+
* off 不发参数;low / high / max 原样发给 OpenAI 兼容端点。
|
|
145
|
+
* (标准 OpenAI 只定义到 high;若端点不接受 max 会返回 400,可换 high。)
|
|
146
|
+
*/
|
|
147
|
+
function reasoningParams(level: string | undefined): Record<string, unknown> {
|
|
148
|
+
if (level === undefined || level === "" || level === "off") {
|
|
149
|
+
return {}
|
|
150
|
+
}
|
|
151
|
+
return { reasoning_effort: level }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** 把 SDK 异常归一化为错误码 */
|
|
155
|
+
export function mapOpenAIError(error: unknown): string {
|
|
156
|
+
if (error instanceof APIUserAbortError) {
|
|
157
|
+
return OPENAI_CODES.aborted
|
|
158
|
+
}
|
|
159
|
+
// APIConnectionTimeoutError 是 APIConnectionError 的子类,先判断超时
|
|
160
|
+
if (error instanceof APIConnectionTimeoutError) {
|
|
161
|
+
return OPENAI_CODES.timeout
|
|
162
|
+
}
|
|
163
|
+
if (error instanceof APIConnectionError) {
|
|
164
|
+
return OPENAI_CODES.connectionFailed
|
|
165
|
+
}
|
|
166
|
+
if (error instanceof AuthenticationError) {
|
|
167
|
+
return OPENAI_CODES.unauthorized
|
|
168
|
+
}
|
|
169
|
+
if (error instanceof PermissionDeniedError) {
|
|
170
|
+
return OPENAI_CODES.forbidden
|
|
171
|
+
}
|
|
172
|
+
if (error instanceof NotFoundError) {
|
|
173
|
+
return OPENAI_CODES.notFound
|
|
174
|
+
}
|
|
175
|
+
if (error instanceof RateLimitError) {
|
|
176
|
+
return OPENAI_CODES.rateLimited
|
|
177
|
+
}
|
|
178
|
+
if (error instanceof BadRequestError) {
|
|
179
|
+
return OPENAI_CODES.badRequest
|
|
180
|
+
}
|
|
181
|
+
if (error instanceof InternalServerError) {
|
|
182
|
+
return OPENAI_CODES.serverError
|
|
183
|
+
}
|
|
184
|
+
if (error instanceof APIError) {
|
|
185
|
+
return `${OPENAI_CODES.apiError}:${error.status ?? "unknown"}`
|
|
186
|
+
}
|
|
187
|
+
return OPENAI_CODES.requestFailed
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export class OpenAIClient {
|
|
191
|
+
readonly #client: OpenAI
|
|
192
|
+
readonly #settings: OpenAISetting
|
|
193
|
+
readonly #logger: Logger | undefined
|
|
194
|
+
|
|
195
|
+
private constructor(client: OpenAI, settings: OpenAISetting, logger?: Logger) {
|
|
196
|
+
this.#client = client
|
|
197
|
+
this.#settings = settings
|
|
198
|
+
this.#logger = logger
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** 创建客户端。缺少 API key 或 SDK 初始化失败时返回错误码 */
|
|
202
|
+
static create(options: CreateOpenAIClientOptions = {}): Result<OpenAIClient> {
|
|
203
|
+
const settings = options.settings ?? DEFAULT_SETTINGS.openai
|
|
204
|
+
const explicit = options.apiKey === undefined ? null : normalizeApiKey(options.apiKey)
|
|
205
|
+
if (options.apiKey !== undefined && explicit === null) {
|
|
206
|
+
return Result.error<OpenAIClient>(OPENAI_CODES.missingApiKey)
|
|
207
|
+
}
|
|
208
|
+
const apiKey =
|
|
209
|
+
explicit ??
|
|
210
|
+
(options.credentialsFile === undefined
|
|
211
|
+
? resolveApiKeySync()
|
|
212
|
+
: resolveApiKeySync(options.credentialsFile)
|
|
213
|
+
)?.apiKey ??
|
|
214
|
+
null
|
|
215
|
+
if (apiKey === null) {
|
|
216
|
+
return Result.error<OpenAIClient>(OPENAI_CODES.missingApiKey)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const client = new OpenAI({
|
|
221
|
+
apiKey,
|
|
222
|
+
timeout: settings.timeoutMs,
|
|
223
|
+
maxRetries: settings.maxRetries,
|
|
224
|
+
...(settings.baseUrl === null ? {} : { baseURL: settings.baseUrl }),
|
|
225
|
+
...(options.fetch === undefined ? {} : { fetch: options.fetch }),
|
|
226
|
+
})
|
|
227
|
+
return Result.successWithData(new OpenAIClient(client, settings, options.logger))
|
|
228
|
+
} catch {
|
|
229
|
+
return Result.error<OpenAIClient>(OPENAI_CODES.clientInitFailed)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** 单轮文本生成,走 Responses API */
|
|
234
|
+
async respond(input: string, options: RespondOptions = {}): Promise<Result<OpenAIReply>> {
|
|
235
|
+
const model = options.model ?? this.#settings.model
|
|
236
|
+
try {
|
|
237
|
+
const response = await this.#client.responses.create({
|
|
238
|
+
model,
|
|
239
|
+
input,
|
|
240
|
+
...(options.instructions === undefined
|
|
241
|
+
? {}
|
|
242
|
+
: { instructions: options.instructions }),
|
|
243
|
+
...(options.maxOutputTokens === undefined
|
|
244
|
+
? {}
|
|
245
|
+
: { max_output_tokens: options.maxOutputTokens }),
|
|
246
|
+
})
|
|
247
|
+
const text = response.output_text
|
|
248
|
+
if (text === "") {
|
|
249
|
+
return Result.error<OpenAIReply>(OPENAI_CODES.emptyOutput)
|
|
250
|
+
}
|
|
251
|
+
this.#logger?.debug({ model: response.model, id: response.id }, "OpenAI Responses 调用")
|
|
252
|
+
const usage = parseProviderUsage(response.usage)
|
|
253
|
+
return Result.successWithData<OpenAIReply>({
|
|
254
|
+
text,
|
|
255
|
+
model: response.model,
|
|
256
|
+
id: response.id,
|
|
257
|
+
inputTokens: usage?.inputTokens ?? null,
|
|
258
|
+
outputTokens: usage?.outputTokens ?? null,
|
|
259
|
+
...providerUsageFields(usage),
|
|
260
|
+
})
|
|
261
|
+
} catch (error) {
|
|
262
|
+
return this.failure(error, model)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** 多轮对话,走 Chat Completions API */
|
|
267
|
+
async chat(
|
|
268
|
+
messages: readonly ChatCompletionMessageParam[],
|
|
269
|
+
options: ChatOptions = {},
|
|
270
|
+
): Promise<Result<OpenAIReply>> {
|
|
271
|
+
const model = options.model ?? this.#settings.model
|
|
272
|
+
try {
|
|
273
|
+
const completion = await this.#client.chat.completions.create({
|
|
274
|
+
model,
|
|
275
|
+
messages: [...messages],
|
|
276
|
+
...(options.temperature === undefined ? {} : { temperature: options.temperature }),
|
|
277
|
+
...(options.maxTokens === undefined
|
|
278
|
+
? {}
|
|
279
|
+
: { max_completion_tokens: options.maxTokens }),
|
|
280
|
+
...reasoningParams(options.reasoning),
|
|
281
|
+
})
|
|
282
|
+
const text = completion.choices[0]?.message.content ?? ""
|
|
283
|
+
if (text === "") {
|
|
284
|
+
return Result.error<OpenAIReply>(OPENAI_CODES.emptyOutput)
|
|
285
|
+
}
|
|
286
|
+
this.#logger?.debug({ model: completion.model, id: completion.id }, "OpenAI Chat 调用")
|
|
287
|
+
const usage = parseProviderUsage(completion.usage)
|
|
288
|
+
return Result.successWithData<OpenAIReply>({
|
|
289
|
+
text,
|
|
290
|
+
model: completion.model,
|
|
291
|
+
id: completion.id,
|
|
292
|
+
inputTokens: usage?.inputTokens ?? null,
|
|
293
|
+
outputTokens: usage?.outputTokens ?? null,
|
|
294
|
+
...providerUsageFields(usage),
|
|
295
|
+
})
|
|
296
|
+
} catch (error) {
|
|
297
|
+
return this.failure(error, model)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** 流式 Chat Completions;始终请求流式,便于完整记录 chunk */
|
|
302
|
+
async chatStream(
|
|
303
|
+
messages: readonly ChatCompletionMessageParam[],
|
|
304
|
+
options: ChatStreamOptions = {},
|
|
305
|
+
): Promise<Result<StreamReply>> {
|
|
306
|
+
const model = options.model ?? this.#settings.model
|
|
307
|
+
/* 累积状态放在 try 外:被 signal 中断时也要把已收到的部分交回去 */
|
|
308
|
+
const chunks: OpenAISessionChunk[] = []
|
|
309
|
+
let text = ""
|
|
310
|
+
let reasoning = ""
|
|
311
|
+
let id = ""
|
|
312
|
+
let responseModel = model
|
|
313
|
+
let streamUsage: ProviderUsage | null = null
|
|
314
|
+
let finishReason: string | null = null
|
|
315
|
+
let blockStarted = false
|
|
316
|
+
const toolCallMap = new Map<number, { id: string; name: string; arguments: string }>()
|
|
317
|
+
const collectedToolCalls = (): ToolCall[] =>
|
|
318
|
+
[...toolCallMap.entries()]
|
|
319
|
+
.sort(([left], [right]) => left - right)
|
|
320
|
+
.map(([index, entry]) => ({
|
|
321
|
+
id: entry.id === "" ? `call_${index}` : entry.id,
|
|
322
|
+
name: entry.name,
|
|
323
|
+
arguments: entry.arguments,
|
|
324
|
+
}))
|
|
325
|
+
try {
|
|
326
|
+
const stream = await this.#client.chat.completions.create(
|
|
327
|
+
{
|
|
328
|
+
model,
|
|
329
|
+
messages: [...messages],
|
|
330
|
+
stream: true,
|
|
331
|
+
stream_options: { include_usage: true },
|
|
332
|
+
...(options.temperature === undefined
|
|
333
|
+
? {}
|
|
334
|
+
: { temperature: options.temperature }),
|
|
335
|
+
...(options.maxTokens === undefined
|
|
336
|
+
? {}
|
|
337
|
+
: { max_completion_tokens: options.maxTokens }),
|
|
338
|
+
...reasoningParams(options.reasoning),
|
|
339
|
+
...(options.tools === undefined || options.tools.length === 0
|
|
340
|
+
? {}
|
|
341
|
+
: {
|
|
342
|
+
tools: [...options.tools],
|
|
343
|
+
tool_choice: options.toolChoice ?? "auto",
|
|
344
|
+
}),
|
|
345
|
+
},
|
|
346
|
+
// signal 是请求选项(第二个参数),放进 body 只会被当成普通字段忽略
|
|
347
|
+
options.signal === undefined ? {} : { signal: options.signal },
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
const push = (chunk: Record<string, unknown>): void => {
|
|
351
|
+
chunks.push({ type: "chunk", time: Date.now(), chunk })
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
for await (const part of stream) {
|
|
355
|
+
if (part.id !== "") {
|
|
356
|
+
id = part.id
|
|
357
|
+
}
|
|
358
|
+
if (part.model !== "") {
|
|
359
|
+
responseModel = part.model
|
|
360
|
+
}
|
|
361
|
+
if (part.usage !== undefined && part.usage !== null) {
|
|
362
|
+
streamUsage = parseProviderUsage(part.usage)
|
|
363
|
+
push({
|
|
364
|
+
type: "usage",
|
|
365
|
+
usage: {
|
|
366
|
+
inputTokens: streamUsage?.inputTokens ?? null,
|
|
367
|
+
outputTokens: streamUsage?.outputTokens ?? null,
|
|
368
|
+
totalTokens: part.usage.total_tokens,
|
|
369
|
+
...(streamUsage?.cacheReadTokens === null ||
|
|
370
|
+
streamUsage?.cacheReadTokens === undefined
|
|
371
|
+
? {}
|
|
372
|
+
: { cacheReadTokens: streamUsage.cacheReadTokens }),
|
|
373
|
+
...(streamUsage?.cacheWriteTokens === null ||
|
|
374
|
+
streamUsage?.cacheWriteTokens === undefined
|
|
375
|
+
? {}
|
|
376
|
+
: { cacheWriteTokens: streamUsage.cacheWriteTokens }),
|
|
377
|
+
},
|
|
378
|
+
})
|
|
379
|
+
}
|
|
380
|
+
const choice = part.choices[0]
|
|
381
|
+
if (choice === undefined) {
|
|
382
|
+
continue
|
|
383
|
+
}
|
|
384
|
+
if (choice.finish_reason !== null && choice.finish_reason !== undefined) {
|
|
385
|
+
finishReason = choice.finish_reason
|
|
386
|
+
}
|
|
387
|
+
for (const toolCallDelta of choice.delta.tool_calls ?? []) {
|
|
388
|
+
const index = toolCallDelta.index ?? toolCallMap.size
|
|
389
|
+
const entry = toolCallMap.get(index) ?? {
|
|
390
|
+
id: "",
|
|
391
|
+
name: "",
|
|
392
|
+
arguments: "",
|
|
393
|
+
}
|
|
394
|
+
if (typeof toolCallDelta.id === "string") {
|
|
395
|
+
entry.id = toolCallDelta.id
|
|
396
|
+
}
|
|
397
|
+
if (typeof toolCallDelta.function?.name === "string") {
|
|
398
|
+
entry.name += toolCallDelta.function.name
|
|
399
|
+
}
|
|
400
|
+
if (typeof toolCallDelta.function?.arguments === "string") {
|
|
401
|
+
entry.arguments += toolCallDelta.function.arguments
|
|
402
|
+
}
|
|
403
|
+
toolCallMap.set(index, entry)
|
|
404
|
+
push({
|
|
405
|
+
type: "tool-call-delta",
|
|
406
|
+
index,
|
|
407
|
+
id: toolCallDelta.id ?? null,
|
|
408
|
+
name: toolCallDelta.function?.name ?? null,
|
|
409
|
+
argumentsDelta: toolCallDelta.function?.arguments ?? null,
|
|
410
|
+
})
|
|
411
|
+
}
|
|
412
|
+
// 推理模型(DeepSeek 等)把思考内容放在 delta.reasoning_content,
|
|
413
|
+
// 部分网关用 delta.reasoning;两者都按增量拼接,不进正文
|
|
414
|
+
const rawDelta = choice.delta as {
|
|
415
|
+
readonly content?: unknown
|
|
416
|
+
readonly reasoning_content?: unknown
|
|
417
|
+
readonly reasoning?: unknown
|
|
418
|
+
}
|
|
419
|
+
const reasoningDelta =
|
|
420
|
+
typeof rawDelta.reasoning_content === "string"
|
|
421
|
+
? rawDelta.reasoning_content
|
|
422
|
+
: typeof rawDelta.reasoning === "string"
|
|
423
|
+
? rawDelta.reasoning
|
|
424
|
+
: ""
|
|
425
|
+
if (reasoningDelta !== "") {
|
|
426
|
+
reasoning += reasoningDelta
|
|
427
|
+
push({ type: "reasoning-delta", index: 1, text: reasoningDelta })
|
|
428
|
+
options.onReasoning?.(reasoningDelta)
|
|
429
|
+
}
|
|
430
|
+
const delta = rawDelta.content
|
|
431
|
+
if (typeof delta === "string" && delta !== "") {
|
|
432
|
+
if (!blockStarted) {
|
|
433
|
+
push({ type: "block-start", index: 0, blockType: "text" })
|
|
434
|
+
blockStarted = true
|
|
435
|
+
}
|
|
436
|
+
text += delta
|
|
437
|
+
push({ type: "text-delta", index: 0, text: delta })
|
|
438
|
+
options.onText?.(delta)
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (!blockStarted) {
|
|
443
|
+
push({ type: "block-start", index: 0, blockType: "text" })
|
|
444
|
+
push({ type: "block-end", index: 0, block: { type: "text", text } })
|
|
445
|
+
} else {
|
|
446
|
+
push({ type: "block-end", index: 0, block: { type: "text", text } })
|
|
447
|
+
}
|
|
448
|
+
const toolCalls = collectedToolCalls()
|
|
449
|
+
for (const toolCall of toolCalls) {
|
|
450
|
+
push({ type: "tool-call", ...toolCall })
|
|
451
|
+
}
|
|
452
|
+
push({ type: "finish", reason: { kind: finishReason ?? "stop" } })
|
|
453
|
+
|
|
454
|
+
if (text === "" && toolCalls.length === 0) {
|
|
455
|
+
return Result.error<StreamReply>(OPENAI_CODES.emptyOutput)
|
|
456
|
+
}
|
|
457
|
+
this.#logger?.debug({ model: responseModel, id }, "OpenAI Chat 流式调用")
|
|
458
|
+
return Result.successWithData<StreamReply>({
|
|
459
|
+
reply: {
|
|
460
|
+
text,
|
|
461
|
+
model: responseModel,
|
|
462
|
+
id,
|
|
463
|
+
inputTokens: streamUsage?.inputTokens ?? null,
|
|
464
|
+
outputTokens: streamUsage?.outputTokens ?? null,
|
|
465
|
+
...providerUsageFields(streamUsage),
|
|
466
|
+
},
|
|
467
|
+
chunks,
|
|
468
|
+
toolCalls,
|
|
469
|
+
reasoning,
|
|
470
|
+
})
|
|
471
|
+
} catch (error) {
|
|
472
|
+
if (options.signal?.aborted === true) {
|
|
473
|
+
/*
|
|
474
|
+
* 用户主动停止:不当作失败。把已经收到的正文/思考/工具调用照常交回去,
|
|
475
|
+
* 由调用方按「已停止」收尾——半截回复比清空更有用。
|
|
476
|
+
*/
|
|
477
|
+
this.#logger?.debug({ model }, "OpenAI 流式调用被用户中断")
|
|
478
|
+
return Result.successWithData<StreamReply>({
|
|
479
|
+
interrupted: true,
|
|
480
|
+
reply: {
|
|
481
|
+
text,
|
|
482
|
+
model: responseModel,
|
|
483
|
+
id,
|
|
484
|
+
inputTokens: streamUsage?.inputTokens ?? null,
|
|
485
|
+
outputTokens: streamUsage?.outputTokens ?? null,
|
|
486
|
+
...providerUsageFields(streamUsage),
|
|
487
|
+
},
|
|
488
|
+
chunks,
|
|
489
|
+
toolCalls: collectedToolCalls(),
|
|
490
|
+
reasoning,
|
|
491
|
+
})
|
|
492
|
+
}
|
|
493
|
+
const code = mapOpenAIError(error)
|
|
494
|
+
const status = error instanceof APIError ? (error.status ?? null) : null
|
|
495
|
+
const reason = error instanceof Error ? error.message : String(error)
|
|
496
|
+
this.#logger?.warn({ code, model, status, reason }, "OpenAI 流式调用失败")
|
|
497
|
+
return Result.error<StreamReply>(code)
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** 拉取供应商的模型 id 列表(用于自定义供应商的“拉取模型”) */
|
|
502
|
+
async listModels(): Promise<Result<readonly string[]>> {
|
|
503
|
+
try {
|
|
504
|
+
const page = await this.#client.models.list()
|
|
505
|
+
const models = (page.data ?? [])
|
|
506
|
+
.map((item) => (typeof item?.id === "string" ? item.id.trim() : ""))
|
|
507
|
+
.filter((id) => id !== "")
|
|
508
|
+
if (models.length === 0) {
|
|
509
|
+
return Result.error<readonly string[]>(OPENAI_CODES.emptyOutput)
|
|
510
|
+
}
|
|
511
|
+
return Result.successWithData<readonly string[]>(models.sort())
|
|
512
|
+
} catch (error) {
|
|
513
|
+
const code = mapOpenAIError(error)
|
|
514
|
+
const reason = error instanceof Error ? error.message : String(error)
|
|
515
|
+
this.#logger?.warn({ code, reason }, "拉取模型列表失败")
|
|
516
|
+
return Result.error<readonly string[]>(code)
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** 轻量连通性测试:拉模型列表,不产生对话费用 */
|
|
521
|
+
async testConnection(): Promise<Result<{ readonly models: number }>> {
|
|
522
|
+
try {
|
|
523
|
+
const page = await this.#client.models.list()
|
|
524
|
+
const models = Array.isArray(page.data) ? page.data.length : 0
|
|
525
|
+
if (models === 0) {
|
|
526
|
+
return Result.error<{ readonly models: number }>(OPENAI_CODES.emptyOutput)
|
|
527
|
+
}
|
|
528
|
+
return Result.successWithData<{ readonly models: number }>({ models })
|
|
529
|
+
} catch (error) {
|
|
530
|
+
const code = mapOpenAIError(error)
|
|
531
|
+
const reason = error instanceof Error ? error.message : String(error)
|
|
532
|
+
this.#logger?.warn({ code, reason }, "OpenAI 连通性测试失败")
|
|
533
|
+
return Result.error<{ readonly models: number }>(code)
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
private failure(error: unknown, model: string): Result<OpenAIReply> {
|
|
538
|
+
const code = mapOpenAIError(error)
|
|
539
|
+
const status = error instanceof APIError ? (error.status ?? null) : null
|
|
540
|
+
const reason = error instanceof Error ? error.message : String(error)
|
|
541
|
+
this.#logger?.warn({ code, model, status, reason }, "OpenAI 调用失败")
|
|
542
|
+
return Result.error<OpenAIReply>(code)
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
let current: OpenAIClient | null = null
|
|
547
|
+
|
|
548
|
+
/** 初始化全局客户端,已初始化时直接返回 */
|
|
549
|
+
export function initOpenAIClient(options: CreateOpenAIClientOptions = {}): Result<OpenAIClient> {
|
|
550
|
+
if (current !== null) {
|
|
551
|
+
return Result.successWithData(current)
|
|
552
|
+
}
|
|
553
|
+
const created = OpenAIClient.create(options)
|
|
554
|
+
if (created.isSuccess()) {
|
|
555
|
+
current = created.getData()
|
|
556
|
+
}
|
|
557
|
+
return created
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** 取全局客户端,未初始化时抛错 */
|
|
561
|
+
export function getOpenAIClient(): OpenAIClient {
|
|
562
|
+
if (current === null) {
|
|
563
|
+
throw new Error("OpenAI 客户端尚未初始化:先调用 initOpenAIClient()")
|
|
564
|
+
}
|
|
565
|
+
return current
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** 清空全局客户端,供测试与关停使用 */
|
|
569
|
+
export function resetOpenAIClient(): void {
|
|
570
|
+
current = null
|
|
571
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* HTTP/HTTPS 代理。
|
|
3
|
+
* 配置来自 app-setting.json 的 network.proxy;通过 undici 的 ProxyAgent 生成自定义 fetch。
|
|
4
|
+
* 只支持 HTTP/HTTPS 代理,SOCKS5 后续再加。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ProxyAgent, fetch as undiciFetch } from "undici"
|
|
8
|
+
|
|
9
|
+
import type { ProxySetting } from "../config/types.ts"
|
|
10
|
+
|
|
11
|
+
/** 从 fetch 入参中取绝对 URL;取不到时返回 null */
|
|
12
|
+
function toUrl(input: string | URL | Request): URL | null {
|
|
13
|
+
if (typeof input === "string") {
|
|
14
|
+
try {
|
|
15
|
+
return new URL(input)
|
|
16
|
+
} catch {
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (input instanceof URL) {
|
|
21
|
+
return input
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
return new URL(input.url)
|
|
25
|
+
} catch {
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizePattern(pattern: string): string {
|
|
31
|
+
return pattern
|
|
32
|
+
.trim()
|
|
33
|
+
.toLowerCase()
|
|
34
|
+
.replace(/^\*?\./, "")
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 判断 URL 是否命中 noProxy。支持 `*`、`example.com`、`.example.com`、子域后缀 */
|
|
38
|
+
export function shouldBypassProxy(url: URL, noProxy: readonly string[]): boolean {
|
|
39
|
+
const host = url.hostname.toLowerCase()
|
|
40
|
+
for (const raw of noProxy) {
|
|
41
|
+
const pattern = normalizePattern(raw)
|
|
42
|
+
if (pattern === "") {
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
if (pattern === "*") {
|
|
46
|
+
return true
|
|
47
|
+
}
|
|
48
|
+
if (host === pattern || host.endsWith(`.${pattern}`)) {
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 生成走代理的 fetch;proxy 为 null 时返回 undefined,调用方沿用全局 fetch。
|
|
57
|
+
* OpenAI SDK 接受自定义 fetch,这里是唯一注入点。
|
|
58
|
+
*/
|
|
59
|
+
export function createProxyFetch(proxy: ProxySetting | null): typeof undiciFetch | undefined {
|
|
60
|
+
if (proxy === null) {
|
|
61
|
+
return undefined
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const agent = new ProxyAgent({ uri: proxy.url })
|
|
65
|
+
const proxyFetch = (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
|
|
66
|
+
const url = toUrl(input)
|
|
67
|
+
// 命中 noProxy 时用全局 fetch,避免代理自身不可达的主机被绕路
|
|
68
|
+
if (url !== null && shouldBypassProxy(url, proxy.noProxy)) {
|
|
69
|
+
return fetch(input as string, init as RequestInit) as Promise<Response>
|
|
70
|
+
}
|
|
71
|
+
return undiciFetch(
|
|
72
|
+
input as never,
|
|
73
|
+
{
|
|
74
|
+
...(init as Record<string, unknown>),
|
|
75
|
+
dispatcher: agent,
|
|
76
|
+
} as never,
|
|
77
|
+
) as Promise<Response>
|
|
78
|
+
}
|
|
79
|
+
return proxyFetch as unknown as typeof undiciFetch
|
|
80
|
+
}
|