@open-mercato/ai-assistant 0.6.1-develop.3291.1.6fad645fd0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +30 -4
- package/dist/frontend/components/AiChatButton.js +3 -2
- package/dist/frontend/components/AiChatButton.js.map +2 -2
- package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.js +364 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.js.map +7 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js +7 -7
- package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js.map +2 -2
- package/dist/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.js +182 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.js +316 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js +8 -7
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/chat/route.js +43 -20
- package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/settings/route.js +4 -3
- package/dist/modules/ai_assistant/api/settings/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/usage/daily/route.js +111 -0
- package/dist/modules/ai_assistant/api/usage/daily/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/usage/sessions/[sessionId]/route.js +108 -0
- package/dist/modules/ai_assistant/api/usage/sessions/[sessionId]/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/usage/sessions/route.js +153 -0
- package/dist/modules/ai_assistant/api/usage/sessions/route.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js +335 -38
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js +2 -7
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js +44 -35
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/AiUsageStatsPageClient.js +282 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/AiUsageStatsPageClient.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.js +10 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.meta.js +25 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.meta.js.map +7 -0
- package/dist/modules/ai_assistant/cli.js +12 -0
- package/dist/modules/ai_assistant/cli.js.map +2 -2
- package/dist/modules/ai_assistant/components/AiAssistantSettingsPageClient.js.map +1 -1
- package/dist/modules/ai_assistant/data/entities.js +177 -1
- package/dist/modules/ai_assistant/data/entities.js.map +2 -2
- package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js +104 -2
- package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js.map +2 -2
- package/dist/modules/ai_assistant/data/repositories/AiTokenUsageRepository.js +168 -0
- package/dist/modules/ai_assistant/data/repositories/AiTokenUsageRepository.js.map +7 -0
- package/dist/modules/ai_assistant/events.js +8 -0
- package/dist/modules/ai_assistant/events.js.map +2 -2
- package/dist/modules/ai_assistant/i18n/de.json +74 -1
- package/dist/modules/ai_assistant/i18n/en.json +74 -1
- package/dist/modules/ai_assistant/i18n/es.json +75 -2
- package/dist/modules/ai_assistant/i18n/pl.json +74 -1
- package/dist/modules/ai_assistant/lib/agent-policy.js.map +2 -2
- package/dist/modules/ai_assistant/lib/agent-runtime.js +588 -23
- package/dist/modules/ai_assistant/lib/agent-runtime.js.map +3 -3
- package/dist/modules/ai_assistant/lib/agent-tools.js +6 -1
- package/dist/modules/ai_assistant/lib/agent-tools.js.map +2 -2
- package/dist/modules/ai_assistant/lib/ai-agent-definition.js.map +2 -2
- package/dist/modules/ai_assistant/lib/model-factory.js +63 -22
- package/dist/modules/ai_assistant/lib/model-factory.js.map +2 -2
- package/dist/modules/ai_assistant/lib/token-usage-recorder.js +78 -0
- package/dist/modules/ai_assistant/lib/token-usage-recorder.js.map +7 -0
- package/dist/modules/ai_assistant/lib/usage-serialization.js +33 -0
- package/dist/modules/ai_assistant/lib/usage-serialization.js.map +7 -0
- package/dist/modules/ai_assistant/migrations/Migration20260508160000_ai_agent_loop_overrides.js +25 -0
- package/dist/modules/ai_assistant/migrations/Migration20260508160000_ai_agent_loop_overrides.js.map +7 -0
- package/dist/modules/ai_assistant/migrations/Migration20260508170000_ai_token_usage.js +88 -0
- package/dist/modules/ai_assistant/migrations/Migration20260508170000_ai_token_usage.js.map +7 -0
- package/dist/modules/ai_assistant/setup.js +34 -0
- package/dist/modules/ai_assistant/setup.js.map +2 -2
- package/dist/modules/ai_assistant/workers/ai-token-usage-prune.js +114 -0
- package/dist/modules/ai_assistant/workers/ai-token-usage-prune.js.map +7 -0
- package/generated/entities/ai_agent_runtime_override/index.ts +7 -0
- package/generated/entities/ai_token_usage_daily/index.ts +16 -0
- package/generated/entities/ai_token_usage_event/index.ts +19 -0
- package/generated/entities.ids.generated.ts +2 -0
- package/generated/entity-fields-registry.ts +47 -1
- package/package.json +15 -7
- package/src/frontend/components/AiChatButton.tsx +3 -2
- package/src/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.ts +521 -0
- package/src/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.ts +8 -8
- package/src/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.ts +231 -0
- package/src/modules/ai_assistant/__tests__/events.test.ts +4 -3
- package/src/modules/ai_assistant/__tests__/settings-page-logic.test.ts +5 -5
- package/src/modules/ai_assistant/__tests__/token-usage-recorder.test.ts +109 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.ts +388 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/__tests__/route.test.ts +5 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/route.ts +8 -7
- package/src/modules/ai_assistant/api/ai/chat/__tests__/route.test.ts +102 -5
- package/src/modules/ai_assistant/api/ai/chat/route.ts +55 -18
- package/src/modules/ai_assistant/api/settings/route.ts +5 -3
- package/src/modules/ai_assistant/api/usage/daily/__tests__/route.test.ts +159 -0
- package/src/modules/ai_assistant/api/usage/daily/route.ts +126 -0
- package/src/modules/ai_assistant/api/usage/sessions/[sessionId]/__tests__/route.test.ts +143 -0
- package/src/modules/ai_assistant/api/usage/sessions/[sessionId]/route.ts +130 -0
- package/src/modules/ai_assistant/api/usage/sessions/__tests__/route.test.ts +123 -0
- package/src/modules/ai_assistant/api/usage/sessions/route.ts +184 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx +372 -16
- package/src/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.tsx +1 -4
- package/src/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.tsx +26 -9
- package/src/modules/ai_assistant/backend/config/ai-assistant/usage/AiUsageStatsPageClient.tsx +469 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/usage/page.meta.ts +23 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/usage/page.tsx +12 -0
- package/src/modules/ai_assistant/cli.ts +18 -0
- package/src/modules/ai_assistant/components/AiAssistantSettingsPageClient.tsx +1 -1
- package/src/modules/ai_assistant/data/entities.ts +237 -0
- package/src/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.ts +135 -3
- package/src/modules/ai_assistant/data/repositories/AiTokenUsageRepository.ts +213 -0
- package/src/modules/ai_assistant/data/repositories/__tests__/AiAgentRuntimeOverrideRepository.test.ts +223 -0
- package/src/modules/ai_assistant/data/repositories/__tests__/AiTokenUsageRepository.test.ts +58 -0
- package/src/modules/ai_assistant/events.ts +8 -0
- package/src/modules/ai_assistant/i18n/de.json +74 -1
- package/src/modules/ai_assistant/i18n/en.json +74 -1
- package/src/modules/ai_assistant/i18n/es.json +75 -2
- package/src/modules/ai_assistant/i18n/pl.json +74 -1
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase0.test.ts +439 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase1.test.ts +243 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase2.test.ts +388 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase3.test.ts +359 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-phase4a.test.ts +2 -2
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime.test.ts +2 -1
- package/src/modules/ai_assistant/lib/__tests__/max-steps-budget.integration.test.ts +12 -13
- package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +77 -14
- package/src/modules/ai_assistant/lib/agent-policy.ts +9 -0
- package/src/modules/ai_assistant/lib/agent-runtime.ts +1148 -43
- package/src/modules/ai_assistant/lib/agent-tools.ts +5 -1
- package/src/modules/ai_assistant/lib/ai-agent-definition.ts +289 -2
- package/src/modules/ai_assistant/lib/model-factory.ts +128 -43
- package/src/modules/ai_assistant/lib/token-usage-recorder.ts +122 -0
- package/src/modules/ai_assistant/lib/usage-serialization.ts +29 -0
- package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +791 -0
- package/src/modules/ai_assistant/migrations/Migration20260508160000_ai_agent_loop_overrides.ts +25 -0
- package/src/modules/ai_assistant/migrations/Migration20260508170000_ai_token_usage.ts +89 -0
- package/src/modules/ai_assistant/setup.ts +49 -0
- package/src/modules/ai_assistant/workers/__tests__/ai-token-usage-prune.test.ts +144 -0
- package/src/modules/ai_assistant/workers/ai-token-usage-prune.ts +188 -0
|
@@ -127,10 +127,14 @@ function toPolicyAuthContext(ctx: AiChatRequestContext): {
|
|
|
127
127
|
* `^[a-zA-Z0-9_-]+$`; dots are replaced with double underscores (`__`).
|
|
128
128
|
* Anthropic and Google accept both formats, so this is safe across providers.
|
|
129
129
|
*/
|
|
130
|
-
function sanitizeToolNameForModel(name: string): string {
|
|
130
|
+
export function sanitizeToolNameForModel(name: string): string {
|
|
131
131
|
return name.replace(/\./g, '__')
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
export function desanitizeToolNameForDisplay(name: string): string {
|
|
135
|
+
return name.replace(/__/g, '.')
|
|
136
|
+
}
|
|
137
|
+
|
|
134
138
|
function formatToolResult(result: unknown): string {
|
|
135
139
|
if (result === null || result === undefined) return 'No result returned'
|
|
136
140
|
if (typeof result === 'string') return result
|
|
@@ -1,8 +1,246 @@
|
|
|
1
1
|
import type { AwilixContainer } from 'awilix'
|
|
2
2
|
import type { ZodTypeAny } from 'zod'
|
|
3
|
+
import type {
|
|
4
|
+
PrepareStepFunction,
|
|
5
|
+
GenerateTextOnStepFinishCallback,
|
|
6
|
+
GenerateTextOnStepStartCallback,
|
|
7
|
+
GenerateTextOnToolCallStartCallback,
|
|
8
|
+
GenerateTextOnToolCallFinishCallback,
|
|
9
|
+
ToolCallRepairFunction,
|
|
10
|
+
StopCondition,
|
|
11
|
+
ToolChoice,
|
|
12
|
+
ToolSet,
|
|
13
|
+
} from 'ai'
|
|
3
14
|
|
|
4
15
|
export type AiAgentExecutionMode = 'chat' | 'object'
|
|
5
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Selects the underlying Vercel AI SDK dispatch strategy for this agent.
|
|
19
|
+
*
|
|
20
|
+
* - `'stream-text'` (default): the runtime calls `streamText(...)` directly on
|
|
21
|
+
* every turn. All loop primitives are supported: `prepareStep`, `stopWhen`,
|
|
22
|
+
* `repairToolCall`, `activeTools`, `toolChoice`.
|
|
23
|
+
*
|
|
24
|
+
* - `'tool-loop-agent'`: the runtime constructs a `ToolLoopAgent`
|
|
25
|
+
* (`Experimental_Agent`) once and dispatches via `agent.generate(...)` /
|
|
26
|
+
* `agent.stream(...)` per turn. The wrapper-owned `prepareStep` (security-
|
|
27
|
+
* critical for mutation-approval) is supplied at construction via
|
|
28
|
+
* `settings.prepareStep`. `stopWhen` is similarly wired at construction.
|
|
29
|
+
* The `prepareCall` hook is used for per-turn narrowing of `model`, `tools`,
|
|
30
|
+
* `stopWhen`, `activeTools`, and `providerOptions`; `prepareStep` is NOT in
|
|
31
|
+
* its `Pick` list and MUST NOT be threaded through it.
|
|
32
|
+
*
|
|
33
|
+
* Note: the current SDK version ships `experimental_repairToolCall` on
|
|
34
|
+
* `ToolLoopAgentSettings`, so `repairToolCall` is technically reachable via
|
|
35
|
+
* this engine. The `loop.repairToolCall` JSDoc retains a caveat reflecting
|
|
36
|
+
* the spec's documented limitation, which was written against an earlier SDK
|
|
37
|
+
* snapshot where the setting was absent — use with awareness that SDK
|
|
38
|
+
* behaviour may differ across versions.
|
|
39
|
+
*
|
|
40
|
+
* Phase 5 of spec `2026-04-28-ai-agents-agentic-loop-controls`.
|
|
41
|
+
*/
|
|
42
|
+
export type AiAgentExecutionEngine = 'stream-text' | 'tool-loop-agent'
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A serializable stop condition for the agentic loop. The `kind` field
|
|
46
|
+
* determines which Vercel AI SDK helper is used at runtime:
|
|
47
|
+
* - `stepCount` → `stepCountIs(count)` — the loop stops after N steps.
|
|
48
|
+
* - `hasToolCall` → `hasToolCall(toolName)` — the loop stops immediately
|
|
49
|
+
* after the model emits a tool call for the named tool.
|
|
50
|
+
* - `custom` — a raw `StopCondition<ToolSet>` predicate supplied in code.
|
|
51
|
+
* NOT valid from JSON-only override sources (tenant DB overrides); only
|
|
52
|
+
* accepted when declared directly in `agent.loop` or a `runAiAgentText`
|
|
53
|
+
* caller override.
|
|
54
|
+
*
|
|
55
|
+
* Phase 0 of spec `2026-04-28-ai-agents-agentic-loop-controls`.
|
|
56
|
+
*/
|
|
57
|
+
export type AiAgentLoopStopCondition =
|
|
58
|
+
| { kind: 'stepCount'; count: number }
|
|
59
|
+
| { kind: 'hasToolCall'; toolName: string }
|
|
60
|
+
| { kind: 'custom'; stop: StopCondition<ToolSet> }
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Budget limits for the agentic loop turn. When any limit is exceeded the
|
|
64
|
+
* wrapper's `prepareStep`/`onStepFinish` aborts the turn via the per-turn
|
|
65
|
+
* `AbortController` and the loop terminates with a `loop_budget_exceeded`
|
|
66
|
+
* finish condition.
|
|
67
|
+
*
|
|
68
|
+
* Budget enforcement is implemented in Phase 1782-3; for Phases 0–2 the
|
|
69
|
+
* fields are accepted and forwarded to the prepared-options bag but are not
|
|
70
|
+
* actively enforced.
|
|
71
|
+
*
|
|
72
|
+
* Phase 0 of spec `2026-04-28-ai-agents-agentic-loop-controls`.
|
|
73
|
+
*/
|
|
74
|
+
export interface AiAgentLoopBudget {
|
|
75
|
+
/** Hard cap on tool calls across all steps in this turn. */
|
|
76
|
+
maxToolCalls?: number
|
|
77
|
+
/** Wall-clock cap (ms) per turn; runtime aborts via AbortController. */
|
|
78
|
+
maxWallClockMs?: number
|
|
79
|
+
/** Input+output token cap; aggregated from step `usage` fields. */
|
|
80
|
+
maxTokens?: number
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* First-class loop configuration for an AI agent. Supersedes the flat
|
|
85
|
+
* `maxSteps` alias on `AiAgentDefinition`.
|
|
86
|
+
*
|
|
87
|
+
* All fields are optional; the runtime falls back to the wrapper default
|
|
88
|
+
* (`{ maxSteps: 10 }` for chat, `{ maxSteps: undefined }` for object) when
|
|
89
|
+
* neither the agent nor the caller supplies any loop config.
|
|
90
|
+
*
|
|
91
|
+
* Phase 0 of spec `2026-04-28-ai-agents-agentic-loop-controls`.
|
|
92
|
+
*/
|
|
93
|
+
export interface AiAgentLoopConfig {
|
|
94
|
+
/** Maximum number of agentic steps before the loop is forced to stop. */
|
|
95
|
+
maxSteps?: number
|
|
96
|
+
/**
|
|
97
|
+
* Additional stop conditions. The wrapper ALWAYS composes these with
|
|
98
|
+
* `stepCountIs(maxSteps ?? 10)` so a misconfigured `hasToolCall` for a
|
|
99
|
+
* non-existent tool can never cause an infinite loop (R3 mitigation).
|
|
100
|
+
*/
|
|
101
|
+
stopWhen?: AiAgentLoopStopCondition | AiAgentLoopStopCondition[]
|
|
102
|
+
/**
|
|
103
|
+
* Per-step preparation hook. The wrapper composes this with its own
|
|
104
|
+
* security-critical `prepareStep` that re-asserts the tool allowlist and
|
|
105
|
+
* mutation-approval wrapping per step.
|
|
106
|
+
*
|
|
107
|
+
* Only valid for chat agents. Rejected with `loop_unsupported_in_object_mode`
|
|
108
|
+
* for object-mode agents.
|
|
109
|
+
*/
|
|
110
|
+
prepareStep?: PrepareStepFunction<ToolSet>
|
|
111
|
+
/**
|
|
112
|
+
* Callback fired when a step finishes. The wrapper chains its own
|
|
113
|
+
* aggregation callback (LoopTrace builder) before invoking this one.
|
|
114
|
+
* Exceptions thrown by this callback are caught and logged but do not
|
|
115
|
+
* abort the turn (matching the SDK's own contract).
|
|
116
|
+
*/
|
|
117
|
+
onStepFinish?: GenerateTextOnStepFinishCallback<ToolSet>
|
|
118
|
+
/**
|
|
119
|
+
* Callback fired when a step starts. Forwarded to the AI SDK as
|
|
120
|
+
* `experimental_onStepStart`.
|
|
121
|
+
*/
|
|
122
|
+
onStepStart?: GenerateTextOnStepStartCallback<ToolSet>
|
|
123
|
+
/**
|
|
124
|
+
* Callback fired when a tool call starts. Forwarded to the AI SDK as
|
|
125
|
+
* `experimental_onToolCallStart`.
|
|
126
|
+
*/
|
|
127
|
+
onToolCallStart?: GenerateTextOnToolCallStartCallback<ToolSet>
|
|
128
|
+
/**
|
|
129
|
+
* Callback fired when a tool call finishes. Forwarded to the AI SDK as
|
|
130
|
+
* `experimental_onToolCallFinish`.
|
|
131
|
+
*/
|
|
132
|
+
onToolCallFinish?: GenerateTextOnToolCallFinishCallback<ToolSet>
|
|
133
|
+
/**
|
|
134
|
+
* Tool-call repair function. Forwarded to the AI SDK as
|
|
135
|
+
* `experimental_repairToolCall`.
|
|
136
|
+
*
|
|
137
|
+
* Only valid for chat agents. Rejected with `loop_unsupported_in_object_mode`
|
|
138
|
+
* for object-mode agents.
|
|
139
|
+
*
|
|
140
|
+
* **Engine note**: this primitive is honored under `executionEngine: 'stream-text'`
|
|
141
|
+
* (default). Agents on `'tool-loop-agent'` may not reliably support
|
|
142
|
+
* `repairToolCall` across all SDK versions — if you require it, use the
|
|
143
|
+
* default `stream-text` engine until support is confirmed stable on the
|
|
144
|
+
* `ToolLoopAgent` class.
|
|
145
|
+
*
|
|
146
|
+
* Phase 5 of spec `2026-04-28-ai-agents-agentic-loop-controls`.
|
|
147
|
+
*/
|
|
148
|
+
repairToolCall?: ToolCallRepairFunction<ToolSet>
|
|
149
|
+
/**
|
|
150
|
+
* Narrow the active tool surface for each step. Names must be a subset of
|
|
151
|
+
* `agent.allowedTools`; any names outside the allowlist are filtered out
|
|
152
|
+
* with a `loop:active_tools_filtered` warning.
|
|
153
|
+
*
|
|
154
|
+
* Only valid for chat agents. Rejected with `loop_unsupported_in_object_mode`
|
|
155
|
+
* for object-mode agents.
|
|
156
|
+
*/
|
|
157
|
+
activeTools?: string[]
|
|
158
|
+
/**
|
|
159
|
+
* Tool choice strategy forwarded to the AI SDK on each step.
|
|
160
|
+
*
|
|
161
|
+
* Only valid for chat agents. Rejected with `loop_unsupported_in_object_mode`
|
|
162
|
+
* for object-mode agents.
|
|
163
|
+
*/
|
|
164
|
+
toolChoice?: ToolChoice<ToolSet>
|
|
165
|
+
/** Budget caps for this loop turn. */
|
|
166
|
+
budget?: AiAgentLoopBudget
|
|
167
|
+
/**
|
|
168
|
+
* When `false`, per-call `runAiAgentText({ loop })` / HTTP query-param
|
|
169
|
+
* overrides are rejected with `AgentPolicyError` code
|
|
170
|
+
* `loop_runtime_override_disabled`. Default is `true` (permissive).
|
|
171
|
+
*
|
|
172
|
+
* Agents that pin a loop policy for correctness reasons (e.g. a
|
|
173
|
+
* `stopWhen: hasToolCall(...)` that must not be bypassed by callers)
|
|
174
|
+
* should set this to `false`.
|
|
175
|
+
*/
|
|
176
|
+
allowRuntimeOverride?: boolean
|
|
177
|
+
/**
|
|
178
|
+
* Kill switch — when `true`, the runtime forces `stopWhen: stepCountIs(1)` and
|
|
179
|
+
* ignores all other loop config. Used by the per-tenant operator override to
|
|
180
|
+
* collapse an agent to a single model call (no tool execution) without
|
|
181
|
+
* disabling the agent entirely.
|
|
182
|
+
*
|
|
183
|
+
* Phase 3 of spec `2026-04-28-ai-agents-agentic-loop-controls`.
|
|
184
|
+
*/
|
|
185
|
+
disabled?: boolean
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Per-step record aggregated by the wrapper-owned `onStepFinish` hook into
|
|
190
|
+
* `LoopTrace`. Each completed agentic step produces one record.
|
|
191
|
+
*
|
|
192
|
+
* Phase 4 of spec `2026-04-28-ai-agents-agentic-loop-controls`.
|
|
193
|
+
*/
|
|
194
|
+
export interface LoopStepRecord {
|
|
195
|
+
stepIndex: number
|
|
196
|
+
/** Model id resolved for this step (relevant when prepareStep swaps models). */
|
|
197
|
+
modelId: string
|
|
198
|
+
toolCalls: Array<{
|
|
199
|
+
toolName: string
|
|
200
|
+
args: unknown
|
|
201
|
+
result?: unknown
|
|
202
|
+
error?: { code: string; message: string }
|
|
203
|
+
repairAttempted: boolean
|
|
204
|
+
durationMs: number
|
|
205
|
+
}>
|
|
206
|
+
/** Raw assistant text emitted in this step. */
|
|
207
|
+
textDelta: string
|
|
208
|
+
usage: { inputTokens: number; outputTokens: number }
|
|
209
|
+
finishReason: 'stop' | 'tool-calls' | 'length' | 'content-filter' | 'error'
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Per-turn trace aggregated by the wrapper-owned `buildLoopTraceCollector`.
|
|
214
|
+
* Not persisted — in-memory only; surfaced via the dispatcher SSE stream and
|
|
215
|
+
* the playground/`<AiChat>` debug panel.
|
|
216
|
+
*
|
|
217
|
+
* Phase 4 of spec `2026-04-28-ai-agents-agentic-loop-controls`.
|
|
218
|
+
*/
|
|
219
|
+
export interface LoopTrace {
|
|
220
|
+
agentId: string
|
|
221
|
+
/**
|
|
222
|
+
* Stable per-conversation id that ties every turn together. Echoed back on
|
|
223
|
+
* the SSE `loop-finish` event so clients can persist it for subsequent turns.
|
|
224
|
+
*
|
|
225
|
+
* Phase 6.2 of spec `2026-04-28-ai-agents-agentic-loop-controls`.
|
|
226
|
+
*/
|
|
227
|
+
sessionId: string
|
|
228
|
+
turnId: string
|
|
229
|
+
steps: LoopStepRecord[]
|
|
230
|
+
stopReason:
|
|
231
|
+
| 'step-count'
|
|
232
|
+
| 'has-tool-call'
|
|
233
|
+
| 'custom-stop'
|
|
234
|
+
| 'budget-tokens'
|
|
235
|
+
| 'budget-tool-calls'
|
|
236
|
+
| 'budget-wall-clock'
|
|
237
|
+
| 'tenant-disabled'
|
|
238
|
+
| 'finish-reason'
|
|
239
|
+
| 'abort'
|
|
240
|
+
totalDurationMs: number
|
|
241
|
+
totalUsage: { inputTokens: number; outputTokens: number }
|
|
242
|
+
}
|
|
243
|
+
|
|
6
244
|
export type AiAgentMutationPolicy =
|
|
7
245
|
| 'read-only'
|
|
8
246
|
| 'confirm-required'
|
|
@@ -46,6 +284,28 @@ export interface AiAgentDefinition {
|
|
|
46
284
|
allowedTools: string[]
|
|
47
285
|
suggestions?: AiAgentSuggestion[]
|
|
48
286
|
executionMode?: AiAgentExecutionMode
|
|
287
|
+
/**
|
|
288
|
+
* Selects the underlying Vercel AI SDK dispatch strategy for this agent.
|
|
289
|
+
* Defaults to `'stream-text'` — the existing behavior and the only engine
|
|
290
|
+
* with unconditional full primitive coverage (`repairToolCall`, all loop
|
|
291
|
+
* controls).
|
|
292
|
+
*
|
|
293
|
+
* Set to `'tool-loop-agent'` to use the `ToolLoopAgent` (`Experimental_Agent`)
|
|
294
|
+
* class, which is closer to a semantic agent abstraction and receives upcoming
|
|
295
|
+
* SDK features (multi-agent handoff, streaming approval responses) first.
|
|
296
|
+
*
|
|
297
|
+
* **Note on `repairToolCall`**: the current SDK version ships
|
|
298
|
+
* `experimental_repairToolCall` on `ToolLoopAgentSettings`, so the primitive
|
|
299
|
+
* is technically available. However, SDK behaviour is not guaranteed to be
|
|
300
|
+
* identical across versions — prefer `'stream-text'` when `repairToolCall`
|
|
301
|
+
* correctness is critical.
|
|
302
|
+
*
|
|
303
|
+
* This field is opt-in: omitting it leaves the existing `stream-text` path
|
|
304
|
+
* completely unchanged.
|
|
305
|
+
*
|
|
306
|
+
* Phase 5 of spec `2026-04-28-ai-agents-agentic-loop-controls`.
|
|
307
|
+
*/
|
|
308
|
+
executionEngine?: AiAgentExecutionEngine
|
|
49
309
|
/**
|
|
50
310
|
* Optional provider id this agent prefers (e.g. `'openai'`, `'anthropic'`).
|
|
51
311
|
* Must match a registered `LlmProvider.id`. When the named provider is
|
|
@@ -84,15 +344,26 @@ export interface AiAgentDefinition {
|
|
|
84
344
|
defaultBaseUrl?: string
|
|
85
345
|
/**
|
|
86
346
|
* When false, per-request HTTP overrides (query params `provider`, `model`,
|
|
87
|
-
* `baseUrl`) and the per-tenant settings override stored in
|
|
347
|
+
* `baseUrl`, `loopBudget`) and the per-tenant settings override stored in
|
|
88
348
|
* `ai_agent_runtime_overrides` are both suppressed. Steps 1 and 3 of the
|
|
89
|
-
* model-factory resolution chain are skipped for this agent
|
|
349
|
+
* model-factory resolution chain are skipped for this agent, and the
|
|
350
|
+
* `loopBudget` query parameter is ignored by the chat dispatcher.
|
|
90
351
|
*
|
|
91
352
|
* Default is `true` (permissive). Agents that pin a specific model for
|
|
92
353
|
* correctness reasons (e.g. a structured-output agent whose JSON-mode schema
|
|
93
354
|
* only works with one provider) should set this to `false`.
|
|
94
355
|
*
|
|
95
356
|
* Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.
|
|
357
|
+
* Renamed from `allowRuntimeModelOverride` in Phase 4 of spec
|
|
358
|
+
* `2026-04-28-ai-agents-agentic-loop-controls`.
|
|
359
|
+
*/
|
|
360
|
+
allowRuntimeOverride?: boolean
|
|
361
|
+
/**
|
|
362
|
+
* @deprecated Use `allowRuntimeOverride` instead. This alias is kept for
|
|
363
|
+
* one minor release and will be removed in a future version. The runtime
|
|
364
|
+
* checks `allowRuntimeOverride` first; if absent it falls back to this field.
|
|
365
|
+
*
|
|
366
|
+
* Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.
|
|
96
367
|
*/
|
|
97
368
|
allowRuntimeModelOverride?: boolean
|
|
98
369
|
acceptedMediaTypes?: AiAgentAcceptedMediaType[]
|
|
@@ -100,7 +371,23 @@ export interface AiAgentDefinition {
|
|
|
100
371
|
uiParts?: string[]
|
|
101
372
|
readOnly?: boolean
|
|
102
373
|
mutationPolicy?: AiAgentMutationPolicy
|
|
374
|
+
/**
|
|
375
|
+
* @deprecated Use `loop.maxSteps` instead. Honored as alias when `loop` is
|
|
376
|
+
* omitted. When both `maxSteps` and `loop.maxSteps` are specified, `loop.maxSteps`
|
|
377
|
+
* wins. This field will be removed in a future minor release.
|
|
378
|
+
*
|
|
379
|
+
* Phase 0 of spec `2026-04-28-ai-agents-agentic-loop-controls`.
|
|
380
|
+
*/
|
|
103
381
|
maxSteps?: number
|
|
382
|
+
/**
|
|
383
|
+
* First-class agentic loop configuration. Supersedes the flat `maxSteps`
|
|
384
|
+
* alias. The runtime walks a precedence chain (per-call override → tenant
|
|
385
|
+
* DB override → this block → legacy `maxSteps` alias → wrapper default)
|
|
386
|
+
* to resolve the effective loop config for each turn.
|
|
387
|
+
*
|
|
388
|
+
* Phase 0 of spec `2026-04-28-ai-agents-agentic-loop-controls`.
|
|
389
|
+
*/
|
|
390
|
+
loop?: AiAgentLoopConfig
|
|
104
391
|
output?: AiAgentStructuredOutput
|
|
105
392
|
resolvePageContext?: (ctx: AiAgentPageContextInput) => Promise<string | null>
|
|
106
393
|
keywords?: string[]
|
|
@@ -23,15 +23,19 @@
|
|
|
23
23
|
* 4. Global env `OM_AI_MODEL` (canonical) with `OPENCODE_MODEL` kept as
|
|
24
24
|
* a backward-compatibility fallback. Accepts either a plain model id
|
|
25
25
|
* (`gpt-5-mini`) or a slash-qualified id (`openai/gpt-5-mini`).
|
|
26
|
-
* Slash qualifiers consume the provider axis at the same step
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* Slash qualifiers consume the provider axis at the same step. When the
|
|
27
|
+
* selected model source carries a provider hint, the provider/model pair
|
|
28
|
+
* is atomic: an unconfigured hinted provider fails instead of sending
|
|
29
|
+
* the model id to a different configured provider.
|
|
29
30
|
* 5. The configured provider's own default model id
|
|
30
31
|
* (`provider.defaultModel`).
|
|
31
32
|
*
|
|
32
33
|
* Every model-axis source is parsed through {@link parseSlashShorthand}.
|
|
33
34
|
* Resolution walks the chain top-down and takes the first non-null hint as
|
|
34
|
-
* the registry-walk seed
|
|
35
|
+
* the registry-walk seed. If the winning model source also supplies the
|
|
36
|
+
* winning provider hint — either through `<provider>/<model>` or through the
|
|
37
|
+
* same-source provider field/env var — that pair is resolved exactly and
|
|
38
|
+
* never mixed with a fallback provider.
|
|
35
39
|
*
|
|
36
40
|
* Provider-axis seed order (highest priority first):
|
|
37
41
|
* 1. Slash-prefix from `callerOverride` (Phase 1).
|
|
@@ -104,8 +108,10 @@ export interface AiModelFactoryInput {
|
|
|
104
108
|
agentDefaultModel?: string
|
|
105
109
|
/**
|
|
106
110
|
* Agent-level default provider, typically `AiAgentDefinition.defaultProvider`.
|
|
107
|
-
* Named provider id
|
|
108
|
-
*
|
|
111
|
+
* Named provider id. When paired with an agent default model, the pair is
|
|
112
|
+
* resolved exactly and fails if the provider is unconfigured. When used as
|
|
113
|
+
* a provider preference without an agent default model, it can fall through
|
|
114
|
+
* to the next configured provider. Sits between `OM_AI_<MODULE>_PROVIDER`
|
|
109
115
|
* and the global `OM_AI_PROVIDER` in the provider-axis seed list above.
|
|
110
116
|
*
|
|
111
117
|
* Phase 1 of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.
|
|
@@ -149,9 +155,9 @@ export interface AiModelFactoryInput {
|
|
|
149
155
|
* between the caller/request override (step 1–2) and the module-env axis
|
|
150
156
|
* (step 4).
|
|
151
157
|
*
|
|
152
|
-
* Honored ONLY when `
|
|
153
|
-
*
|
|
154
|
-
* does NOT load the row itself.
|
|
158
|
+
* Honored ONLY when `allowRuntimeOverride !== false` on the agent definition
|
|
159
|
+
* (checked via `resolveAllowRuntimeOverride`). The agent runtime is
|
|
160
|
+
* responsible for hydration — the factory does NOT load the row itself.
|
|
155
161
|
*
|
|
156
162
|
* Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.
|
|
157
163
|
*/
|
|
@@ -165,8 +171,9 @@ export interface AiModelFactoryInput {
|
|
|
165
171
|
* (`?provider=`, `?model=`, `?baseUrl=`). Sits at step 1 of the resolution
|
|
166
172
|
* chain — wins over everything else for that turn.
|
|
167
173
|
*
|
|
168
|
-
* Honored ONLY when `
|
|
169
|
-
* The dispatcher validates all three
|
|
174
|
+
* Honored ONLY when `allowRuntimeOverride !== false` on the agent (checked
|
|
175
|
+
* via `resolveAllowRuntimeOverride`). The dispatcher validates all three
|
|
176
|
+
* values before setting this input.
|
|
170
177
|
*
|
|
171
178
|
* Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.
|
|
172
179
|
*/
|
|
@@ -178,8 +185,19 @@ export interface AiModelFactoryInput {
|
|
|
178
185
|
/**
|
|
179
186
|
* When false, steps 1 (requestOverride) and 3 (tenantOverride) of the
|
|
180
187
|
* resolution chain are skipped. Agents that pin a specific model for
|
|
181
|
-
* correctness reasons set `AiAgentDefinition.
|
|
182
|
-
*
|
|
188
|
+
* correctness reasons set `AiAgentDefinition.allowRuntimeOverride = false`.
|
|
189
|
+
* Default behavior (omitted) is permissive (= true).
|
|
190
|
+
*
|
|
191
|
+
* Canonical field (renamed from `allowRuntimeModelOverride` in Phase 4 of
|
|
192
|
+
* spec `2026-04-28-ai-agents-agentic-loop-controls`). The deprecated alias
|
|
193
|
+
* `allowRuntimeModelOverride` is still accepted via the resolution helper
|
|
194
|
+
* {@link resolveAllowRuntimeOverride}.
|
|
195
|
+
*
|
|
196
|
+
* Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.
|
|
197
|
+
*/
|
|
198
|
+
allowRuntimeOverride?: boolean
|
|
199
|
+
/**
|
|
200
|
+
* @deprecated Use `allowRuntimeOverride` instead.
|
|
183
201
|
*
|
|
184
202
|
* Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.
|
|
185
203
|
*/
|
|
@@ -410,6 +428,28 @@ function normalizeProviderHint(
|
|
|
410
428
|
return providerIdAliases(providerId)[0] ?? providerId
|
|
411
429
|
}
|
|
412
430
|
|
|
431
|
+
function resolveRequiredProvider(
|
|
432
|
+
providerId: string,
|
|
433
|
+
registry: AiModelFactoryRegistry,
|
|
434
|
+
env: EnvLookup,
|
|
435
|
+
): LlmProvider | null {
|
|
436
|
+
const resolved = registry.resolveFirstConfigured({ env, order: [providerId] })
|
|
437
|
+
if (resolved?.id === providerId) return resolved
|
|
438
|
+
|
|
439
|
+
const direct = registry.get?.(providerId) ?? null
|
|
440
|
+
if (direct) return direct.isConfigured(env) ? direct : null
|
|
441
|
+
return null
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function requiredProviderMessage(providerId: string, registry: AiModelFactoryRegistry, env: EnvLookup): string {
|
|
445
|
+
const provider = registry.get?.(providerId) ?? null
|
|
446
|
+
const envKey = provider?.getConfiguredEnvKey?.(env)
|
|
447
|
+
const credentialHint = envKey
|
|
448
|
+
? ` Set ${envKey} to use this provider.`
|
|
449
|
+
: ' Configure the matching provider API key to use this provider.'
|
|
450
|
+
return `The resolved model is pinned to provider "${providerId}", but that provider is not configured.${credentialHint} The runtime refuses to send provider-specific model ids to a different provider.`
|
|
451
|
+
}
|
|
452
|
+
|
|
413
453
|
function moduleBaseUrlEnvVarName(moduleId: string): string {
|
|
414
454
|
return `${moduleId.toUpperCase()}_AI_BASE_URL`
|
|
415
455
|
}
|
|
@@ -443,6 +483,23 @@ export function parseSlashShorthand(
|
|
|
443
483
|
return { providerHint: before, modelId: after }
|
|
444
484
|
}
|
|
445
485
|
|
|
486
|
+
/**
|
|
487
|
+
* Resolves the effective `allowRuntimeOverride` flag from an input that may
|
|
488
|
+
* carry either the new canonical name (`allowRuntimeOverride`) or the
|
|
489
|
+
* deprecated alias (`allowRuntimeModelOverride`). The canonical name wins
|
|
490
|
+
* when both are present. Returns `true` (permissive) when neither is set.
|
|
491
|
+
*
|
|
492
|
+
* Exported for test coverage.
|
|
493
|
+
*/
|
|
494
|
+
export function resolveAllowRuntimeOverride(input: {
|
|
495
|
+
allowRuntimeOverride?: boolean
|
|
496
|
+
allowRuntimeModelOverride?: boolean
|
|
497
|
+
}): boolean {
|
|
498
|
+
if (input.allowRuntimeOverride !== undefined) return input.allowRuntimeOverride !== false
|
|
499
|
+
if (input.allowRuntimeModelOverride !== undefined) return input.allowRuntimeModelOverride !== false
|
|
500
|
+
return true
|
|
501
|
+
}
|
|
502
|
+
|
|
446
503
|
/**
|
|
447
504
|
* Creates an {@link AiModelFactory} bound to the DI container. The container
|
|
448
505
|
* reference is accepted for API symmetry with other runtime helpers (and so
|
|
@@ -460,9 +517,9 @@ export function createModelFactory(
|
|
|
460
517
|
return {
|
|
461
518
|
resolveModel(input: AiModelFactoryInput): AiModelResolution {
|
|
462
519
|
const hasModule = typeof input.moduleId === 'string' && input.moduleId.length > 0
|
|
463
|
-
// When
|
|
464
|
-
// (requestOverride) and 3 (tenantOverride)
|
|
465
|
-
const runtimeOverridesAllowed = input
|
|
520
|
+
// When allowRuntimeOverride (or its deprecated alias allowRuntimeModelOverride)
|
|
521
|
+
// is explicitly false, skip steps 1 (requestOverride) and 3 (tenantOverride).
|
|
522
|
+
const runtimeOverridesAllowed = resolveAllowRuntimeOverride(input)
|
|
466
523
|
|
|
467
524
|
// --- Step 1: requestOverride (HTTP query params) — gated by flag ---
|
|
468
525
|
const requestModelRaw = runtimeOverridesAllowed
|
|
@@ -506,10 +563,6 @@ export function createModelFactory(
|
|
|
506
563
|
const agentModelParsed = agentModelRaw ? parseSlashShorthand(agentModelRaw, registry) : null
|
|
507
564
|
const globalModelParsed = globalModelRaw ? parseSlashShorthand(globalModelRaw, registry) : null
|
|
508
565
|
|
|
509
|
-
// --- Provider-axis: walk from highest to lowest priority for the seed.
|
|
510
|
-
// A slash-qualified hint from a model source wins over a plain provider
|
|
511
|
-
// source at the same priority step. We walk top-down and take the first
|
|
512
|
-
// non-null hint.
|
|
513
566
|
const providerOverrideRaw = normalizeOverride(input.providerOverride)
|
|
514
567
|
const moduleProviderRaw = hasModule
|
|
515
568
|
? readModuleProviderEnvOverride(env, input.moduleId!)
|
|
@@ -519,70 +572,102 @@ export function createModelFactory(
|
|
|
519
572
|
// a backward-compatibility fallback through readGlobalProviderFromEnv.
|
|
520
573
|
const globalProviderRaw = readGlobalProviderFromEnv(env, registry)
|
|
521
574
|
|
|
575
|
+
const requestProviderHint = normalizeProviderHint(requestProviderRaw, registry)
|
|
576
|
+
const providerOverrideHint = normalizeProviderHint(providerOverrideRaw, registry)
|
|
577
|
+
const tenantProviderHint = normalizeProviderHint(tenantProviderRaw, registry)
|
|
578
|
+
const moduleProviderHint = normalizeProviderHint(moduleProviderRaw, registry)
|
|
579
|
+
const agentDefaultProviderHint = normalizeProviderHint(agentDefaultProviderRaw, registry)
|
|
580
|
+
|
|
522
581
|
// Walk the provider-axis seed list: slash hint beats plain provider at
|
|
523
582
|
// the same step. We keep only the first (highest-priority) non-null hint.
|
|
524
583
|
const providerHintCandidates: Array<string | null> = [
|
|
525
584
|
requestModelParsed?.providerHint ?? null,
|
|
526
|
-
|
|
585
|
+
requestProviderHint,
|
|
527
586
|
callerParsed?.providerHint ?? null,
|
|
528
|
-
|
|
587
|
+
providerOverrideHint,
|
|
529
588
|
tenantModelParsed?.providerHint ?? null,
|
|
530
|
-
|
|
589
|
+
tenantProviderHint,
|
|
531
590
|
moduleModelParsed?.providerHint ?? null,
|
|
532
|
-
|
|
591
|
+
moduleProviderHint,
|
|
533
592
|
agentModelParsed?.providerHint ?? null,
|
|
534
|
-
|
|
593
|
+
agentDefaultProviderHint,
|
|
535
594
|
globalModelParsed?.providerHint ?? null,
|
|
536
595
|
globalProviderRaw,
|
|
537
596
|
]
|
|
538
597
|
const orderHint = providerHintCandidates.find((hint) => hint !== null) ?? null
|
|
539
598
|
const order = orderHint ? [orderHint] : undefined
|
|
540
599
|
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
throw new AiModelFactoryError(
|
|
544
|
-
'no_provider_configured',
|
|
545
|
-
'No LLM provider is configured. Set OM_AI_PROVIDER (or the legacy OPENCODE_PROVIDER) plus a matching API key such as OPENAI_API_KEY, ANTHROPIC_API_KEY, or GOOGLE_GENERATIVE_AI_API_KEY, then restart the app. See https://docs.openmercato.com/framework/ai-assistant/overview.',
|
|
546
|
-
)
|
|
547
|
-
}
|
|
548
|
-
const apiKey = provider.resolveApiKey(env)
|
|
549
|
-
if (!apiKey) {
|
|
550
|
-
throw new AiModelFactoryError(
|
|
551
|
-
'api_key_missing',
|
|
552
|
-
`LLM provider "${provider.id}" is advertised as configured but resolveApiKey() returned empty.`,
|
|
553
|
-
)
|
|
554
|
-
}
|
|
600
|
+
const pairPlainProviderIfWinning = (providerHint: string | null): string | null =>
|
|
601
|
+
providerHint && providerHint === orderHint ? providerHint : null
|
|
555
602
|
|
|
556
|
-
// --- Model-axis: use the post-parse model id from the winning source.
|
|
557
603
|
let modelId: string
|
|
558
604
|
let source: AiModelResolution['source']
|
|
605
|
+
let pairedProviderHint: string | null = null
|
|
559
606
|
if (requestModelParsed) {
|
|
560
607
|
modelId = requestModelParsed.modelId
|
|
561
608
|
source = 'request_override'
|
|
609
|
+
pairedProviderHint = requestModelParsed.providerHint ?? pairPlainProviderIfWinning(requestProviderHint)
|
|
562
610
|
} else if (callerParsed) {
|
|
563
611
|
modelId = callerParsed.modelId
|
|
564
612
|
source = 'caller_override'
|
|
613
|
+
pairedProviderHint = callerParsed.providerHint ?? pairPlainProviderIfWinning(providerOverrideHint)
|
|
565
614
|
} else if (tenantModelParsed) {
|
|
566
615
|
modelId = tenantModelParsed.modelId
|
|
567
616
|
source = 'tenant_override'
|
|
617
|
+
pairedProviderHint = tenantModelParsed.providerHint ?? pairPlainProviderIfWinning(tenantProviderHint)
|
|
568
618
|
} else if (moduleModelParsed) {
|
|
569
619
|
modelId = moduleModelParsed.modelId
|
|
570
620
|
source = 'module_env'
|
|
621
|
+
pairedProviderHint = moduleModelParsed.providerHint ?? pairPlainProviderIfWinning(moduleProviderHint)
|
|
571
622
|
} else if (agentModelParsed) {
|
|
572
623
|
modelId = agentModelParsed.modelId
|
|
573
624
|
source = 'agent_default'
|
|
625
|
+
pairedProviderHint = agentModelParsed.providerHint ?? pairPlainProviderIfWinning(agentDefaultProviderHint)
|
|
574
626
|
} else if (globalModelParsed) {
|
|
575
627
|
modelId = globalModelParsed.modelId
|
|
576
628
|
source = 'env_default'
|
|
629
|
+
pairedProviderHint = globalModelParsed.providerHint ?? pairPlainProviderIfWinning(globalProviderRaw)
|
|
577
630
|
} else {
|
|
578
|
-
modelId =
|
|
631
|
+
modelId = ''
|
|
579
632
|
source = 'provider_default'
|
|
580
633
|
}
|
|
581
634
|
|
|
635
|
+
// --- Provider-axis: walk from highest to lowest priority for the seed.
|
|
636
|
+
// A slash-qualified hint from a model source wins over a plain provider
|
|
637
|
+
// source at the same priority step. We walk top-down and take the first
|
|
638
|
+
// non-null hint.
|
|
639
|
+
const provider = pairedProviderHint
|
|
640
|
+
? resolveRequiredProvider(pairedProviderHint, registry, env)
|
|
641
|
+
: registry.resolveFirstConfigured({ env, order })
|
|
642
|
+
if (!provider) {
|
|
643
|
+
if (pairedProviderHint) {
|
|
644
|
+
throw new AiModelFactoryError(
|
|
645
|
+
'no_provider_configured',
|
|
646
|
+
requiredProviderMessage(pairedProviderHint, registry, env),
|
|
647
|
+
)
|
|
648
|
+
}
|
|
649
|
+
throw new AiModelFactoryError(
|
|
650
|
+
'no_provider_configured',
|
|
651
|
+
'No LLM provider is configured. Set OM_AI_PROVIDER (or the legacy OPENCODE_PROVIDER) plus a matching API key such as OPENAI_API_KEY, ANTHROPIC_API_KEY, or GOOGLE_GENERATIVE_AI_API_KEY, then restart the app. See https://docs.openmercato.com/framework/ai-assistant/overview.',
|
|
652
|
+
)
|
|
653
|
+
}
|
|
654
|
+
const apiKey = provider.resolveApiKey(env)
|
|
655
|
+
if (!apiKey) {
|
|
656
|
+
throw new AiModelFactoryError(
|
|
657
|
+
'api_key_missing',
|
|
658
|
+
`LLM provider "${provider.id}" is advertised as configured but resolveApiKey() returned empty.`,
|
|
659
|
+
)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// --- Model-axis: use the post-parse model id from the winning source.
|
|
663
|
+
if (source === 'provider_default') {
|
|
664
|
+
modelId = provider.defaultModel
|
|
665
|
+
}
|
|
666
|
+
|
|
582
667
|
// --- BaseURL-axis resolution (highest to lowest priority) ---
|
|
583
|
-
// 1. requestOverride.baseURL (HTTP dispatcher) — gated by
|
|
668
|
+
// 1. requestOverride.baseURL (HTTP dispatcher) — gated by allowRuntimeOverride
|
|
584
669
|
// 2. baseUrlOverride (programmatic caller)
|
|
585
|
-
// 3. tenantOverride.baseURL (DB row) — gated by
|
|
670
|
+
// 3. tenantOverride.baseURL (DB row) — gated by allowRuntimeOverride
|
|
586
671
|
// 4. <MODULE>_AI_BASE_URL env
|
|
587
672
|
// 5. agentDefaultBaseUrl
|
|
588
673
|
// Steps 6-7 (preset env + preset default) are handled inside the adapter's
|