@strav/brain 1.0.0-alpha.40 → 1.0.0-alpha.43
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 +3 -3
- package/src/brain_config.ts +86 -0
- package/src/brain_provider.ts +27 -3
- package/src/drivers/minimax/index.ts +1 -0
- package/src/drivers/minimax/minimax_brain_driver.ts +84 -0
- package/src/drivers/openrouter/index.ts +1 -0
- package/src/drivers/openrouter/openrouter_brain_driver.ts +137 -0
- package/src/drivers/qwen/index.ts +1 -0
- package/src/drivers/qwen/qwen_brain_driver.ts +103 -0
- package/src/index.ts +22 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strav/brain",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.43",
|
|
4
4
|
"description": "Strav AI module — unified Provider interface, BrainManager, threads, prompt caching, tools / agents / MCP. Anthropic + OpenAI providers; Gemini / DeepSeek follow.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"@anthropic-ai/sdk": "^0.100.0",
|
|
27
27
|
"@google/genai": "^2.7.0",
|
|
28
28
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
29
|
-
"@strav/database": "1.0.0-alpha.
|
|
30
|
-
"@strav/kernel": "1.0.0-alpha.
|
|
29
|
+
"@strav/database": "1.0.0-alpha.43",
|
|
30
|
+
"@strav/kernel": "1.0.0-alpha.43",
|
|
31
31
|
"openai": "^6.0.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
package/src/brain_config.ts
CHANGED
|
@@ -151,6 +151,89 @@ export interface OllamaProviderConfig {
|
|
|
151
151
|
defaultTranscribeModel?: string
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Qwen (Alibaba DashScope) driver config — backed by the `openai`
|
|
156
|
+
* SDK pointed at DashScope's OpenAI-compatible `/compatible-mode/v1`
|
|
157
|
+
* endpoint.
|
|
158
|
+
*
|
|
159
|
+
* DashScope publishes regional endpoints (Singapore, Beijing, Hong Kong,
|
|
160
|
+
* US-Virginia). The default is the Singapore endpoint
|
|
161
|
+
* (`dashscope-intl`) to fit Strav's SEA-first positioning; apps in
|
|
162
|
+
* other regions override via `baseUrl`.
|
|
163
|
+
*/
|
|
164
|
+
export interface QwenProviderConfig {
|
|
165
|
+
driver: 'qwen'
|
|
166
|
+
/** API key. Required. Most apps source from `env('DASHSCOPE_API_KEY')` or `env('QWEN_API_KEY')`. */
|
|
167
|
+
apiKey: string
|
|
168
|
+
/**
|
|
169
|
+
* Optional base URL override. Defaults to the Singapore endpoint
|
|
170
|
+
* `https://dashscope-intl.aliyuncs.com/compatible-mode/v1`. Other
|
|
171
|
+
* documented options:
|
|
172
|
+
* - `https://dashscope.aliyuncs.com/compatible-mode/v1` (Beijing)
|
|
173
|
+
* - `https://dashscope-us.aliyuncs.com/compatible-mode/v1` (US/Virginia)
|
|
174
|
+
* - `https://cn-hongkong.dashscope.aliyuncs.com/compatible-mode/v1` (Hong Kong)
|
|
175
|
+
*/
|
|
176
|
+
baseUrl?: string
|
|
177
|
+
/** Default model. Defaults to `qwen-plus` (mid-tier; `qwen-turbo` is cheaper, `qwen-max` is heaviest). */
|
|
178
|
+
defaultModel?: string
|
|
179
|
+
/** Default `max_tokens` for `chat()` calls that don't specify one. */
|
|
180
|
+
defaultMaxTokens?: number
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* MiniMax driver config — backed by the `openai` SDK pointed at
|
|
185
|
+
* MiniMax's OpenAI-compatible endpoint at `https://api.minimax.io/v1`.
|
|
186
|
+
*/
|
|
187
|
+
export interface MiniMaxProviderConfig {
|
|
188
|
+
driver: 'minimax'
|
|
189
|
+
/** API key. Required. Most apps source from `env('MINIMAX_API_KEY')`. */
|
|
190
|
+
apiKey: string
|
|
191
|
+
/** Optional base URL override. Defaults to `https://api.minimax.io/v1`. */
|
|
192
|
+
baseUrl?: string
|
|
193
|
+
/** Default model. Defaults to `MiniMax-M2`. */
|
|
194
|
+
defaultModel?: string
|
|
195
|
+
/** Default `max_tokens` for `chat()` calls that don't specify one. */
|
|
196
|
+
defaultMaxTokens?: number
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* OpenRouter driver config — backed by the `openai` SDK pointed at
|
|
201
|
+
* `https://openrouter.ai/api/v1`. OpenRouter multiplexes 300+ models
|
|
202
|
+
* behind one OpenAI-compatible endpoint; **the surface a given call
|
|
203
|
+
* supports depends on the chosen model**. Pin an explicit model slug
|
|
204
|
+
* (e.g. `anthropic/claude-sonnet-4`, `meta-llama/llama-3.3-70b`) per
|
|
205
|
+
* call; consult `https://openrouter.ai/api/v1/models` for the
|
|
206
|
+
* `supported_parameters` per model. Calls to `embed` / `transcribe`
|
|
207
|
+
* are not exposed; structured output uses the OpenAI-compat
|
|
208
|
+
* tool-forcing pattern and depends on the model supporting
|
|
209
|
+
* function-calling.
|
|
210
|
+
*/
|
|
211
|
+
export interface OpenRouterProviderConfig {
|
|
212
|
+
driver: 'openrouter'
|
|
213
|
+
/** API key. Required. Most apps source from `env('OPENROUTER_API_KEY')`. */
|
|
214
|
+
apiKey: string
|
|
215
|
+
/** Optional base URL override. Defaults to `https://openrouter.ai/api/v1`. */
|
|
216
|
+
baseUrl?: string
|
|
217
|
+
/**
|
|
218
|
+
* Default model. No default — OpenRouter has no canonical pick;
|
|
219
|
+
* apps must choose a slug (e.g. `anthropic/claude-sonnet-4`).
|
|
220
|
+
*/
|
|
221
|
+
defaultModel?: string
|
|
222
|
+
/** Default `max_tokens` for `chat()` calls that don't specify one. */
|
|
223
|
+
defaultMaxTokens?: number
|
|
224
|
+
/**
|
|
225
|
+
* Optional app URL — forwarded as the `HTTP-Referer` header.
|
|
226
|
+
* OpenRouter uses it to attribute traffic on their leaderboard
|
|
227
|
+
* and rankings page.
|
|
228
|
+
*/
|
|
229
|
+
appUrl?: string
|
|
230
|
+
/**
|
|
231
|
+
* Optional app title — forwarded as the `X-Title` header. Paired
|
|
232
|
+
* with `appUrl` for OpenRouter's leaderboard.
|
|
233
|
+
*/
|
|
234
|
+
appTitle?: string
|
|
235
|
+
}
|
|
236
|
+
|
|
154
237
|
export type ProviderConfig =
|
|
155
238
|
| AnthropicProviderConfig
|
|
156
239
|
| OpenAIProviderConfig
|
|
@@ -158,6 +241,9 @@ export type ProviderConfig =
|
|
|
158
241
|
| GeminiProviderConfig
|
|
159
242
|
| DeepSeekProviderConfig
|
|
160
243
|
| OllamaProviderConfig
|
|
244
|
+
| QwenProviderConfig
|
|
245
|
+
| MiniMaxProviderConfig
|
|
246
|
+
| OpenRouterProviderConfig
|
|
161
247
|
|
|
162
248
|
/** Cache-shape defaults applied when `ChatOptions.cache` is omitted. */
|
|
163
249
|
export interface BrainCacheConfig {
|
package/src/brain_provider.ts
CHANGED
|
@@ -25,15 +25,18 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import { type Application, ConfigError, ConfigRepository, ServiceProvider } from '@strav/kernel'
|
|
28
|
-
import { BrainManager } from './brain_manager.ts'
|
|
29
28
|
import type { BrainConfigShape, ProviderConfig } from './brain_config.ts'
|
|
29
|
+
import type { BrainDriver } from './brain_driver.ts'
|
|
30
|
+
import { BrainManager } from './brain_manager.ts'
|
|
30
31
|
import { AnthropicBrainDriver } from './drivers/anthropic/anthropic_brain_driver.ts'
|
|
31
32
|
import { DeepSeekBrainDriver } from './drivers/deepseek/deepseek_brain_driver.ts'
|
|
32
33
|
import { GeminiBrainDriver } from './drivers/gemini/gemini_brain_driver.ts'
|
|
34
|
+
import { MiniMaxBrainDriver } from './drivers/minimax/minimax_brain_driver.ts'
|
|
33
35
|
import { OllamaBrainDriver } from './drivers/ollama/ollama_brain_driver.ts'
|
|
34
36
|
import { OpenAIBrainDriver } from './drivers/openai/openai_brain_driver.ts'
|
|
35
37
|
import { OpenAIResponsesBrainDriver } from './drivers/openai_responses/openai_responses_brain_driver.ts'
|
|
36
|
-
import
|
|
38
|
+
import { OpenRouterBrainDriver } from './drivers/openrouter/openrouter_brain_driver.ts'
|
|
39
|
+
import { QwenBrainDriver } from './drivers/qwen/qwen_brain_driver.ts'
|
|
37
40
|
|
|
38
41
|
export class BrainProvider extends ServiceProvider {
|
|
39
42
|
override readonly name = 'brain'
|
|
@@ -133,10 +136,31 @@ function buildBrainDriver(name: string, config: ProviderConfig): BrainDriver {
|
|
|
133
136
|
)
|
|
134
137
|
}
|
|
135
138
|
return new OllamaBrainDriver(name, config)
|
|
139
|
+
case 'qwen':
|
|
140
|
+
if (!config.apiKey) {
|
|
141
|
+
throw new ConfigError(
|
|
142
|
+
`BrainProvider: qwen provider "${name}" is missing apiKey. Source from env('DASHSCOPE_API_KEY') or env('QWEN_API_KEY').`,
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
return new QwenBrainDriver(name, config)
|
|
146
|
+
case 'minimax':
|
|
147
|
+
if (!config.apiKey) {
|
|
148
|
+
throw new ConfigError(
|
|
149
|
+
`BrainProvider: minimax provider "${name}" is missing apiKey. Source from env('MINIMAX_API_KEY').`,
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
return new MiniMaxBrainDriver(name, config)
|
|
153
|
+
case 'openrouter':
|
|
154
|
+
if (!config.apiKey) {
|
|
155
|
+
throw new ConfigError(
|
|
156
|
+
`BrainProvider: openrouter provider "${name}" is missing apiKey. Source from env('OPENROUTER_API_KEY').`,
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
return new OpenRouterBrainDriver(name, config)
|
|
136
160
|
default: {
|
|
137
161
|
const exhaustiveCheck: never = config
|
|
138
162
|
throw new ConfigError(
|
|
139
|
-
`BrainProvider: unknown driver for provider "${name}". Known drivers: anthropic, openai, openai-responses, google, deepseek, ollama.`,
|
|
163
|
+
`BrainProvider: unknown driver for provider "${name}". Known drivers: anthropic, openai, openai-responses, google, deepseek, ollama, qwen, minimax, openrouter.`,
|
|
140
164
|
)
|
|
141
165
|
// (unreachable — kept for the exhaustive check to fire when a new driver lands)
|
|
142
166
|
// biome-ignore lint/correctness/noUnreachable: kept for the exhaustive-check above
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { MiniMaxBrainDriver } from './minimax_brain_driver.ts'
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `MiniMaxBrainDriver` — `OpenAICompatBrainDriver` pointed at
|
|
3
|
+
* MiniMax's OpenAI-compatible endpoint at `https://api.minimax.io/v1`.
|
|
4
|
+
*
|
|
5
|
+
* MiniMax (M2, abab line) speaks the OpenAI Chat Completions wire
|
|
6
|
+
* format and supports function-calling, so the OpenAI-compat base
|
|
7
|
+
* already covers chat / stream / generate (via `json_object`) /
|
|
8
|
+
* runWithToolsAndSchema (via tool-forcing).
|
|
9
|
+
*
|
|
10
|
+
* MiniMax has a `reasoning_split=true` flag that surfaces the
|
|
11
|
+
* assistant's reasoning trace on a separate `reasoning_details`
|
|
12
|
+
* field. Not wired in V1 — `effort` / `thinking` are stripped by
|
|
13
|
+
* the OpenAI-compat `buildParams` override and apps reach for
|
|
14
|
+
* reasoning surfaces via the Anthropic / Gemini / OpenAI Responses
|
|
15
|
+
* drivers.
|
|
16
|
+
*
|
|
17
|
+
* `embed` / `transcribe` are not exposed via the compatibility
|
|
18
|
+
* endpoint; the inherited stubs are replaced with explicit
|
|
19
|
+
* `BrainError`s so apps see a clear message instead of a wire 404.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type OpenAI from 'openai'
|
|
23
|
+
import type { MiniMaxProviderConfig } from '../../brain_config.ts'
|
|
24
|
+
import { BrainError } from '../../brain_error.ts'
|
|
25
|
+
import type { ResolveMcpToolsOptions } from '../../mcp/resolve_mcp_tools.ts'
|
|
26
|
+
import type {
|
|
27
|
+
AudioSource,
|
|
28
|
+
EmbedOptions,
|
|
29
|
+
EmbedResult,
|
|
30
|
+
TranscribeOptions,
|
|
31
|
+
TranscribeResult,
|
|
32
|
+
} from '../../types.ts'
|
|
33
|
+
import { OpenAICompatBrainDriver } from '../openai_compat/openai_compat_brain_driver.ts'
|
|
34
|
+
|
|
35
|
+
const DEFAULT_MINIMAX_MODEL = 'MiniMax-M2'
|
|
36
|
+
const DEFAULT_MINIMAX_BASE_URL = 'https://api.minimax.io/v1'
|
|
37
|
+
|
|
38
|
+
export interface MiniMaxProviderOptions {
|
|
39
|
+
client?: OpenAI
|
|
40
|
+
/**
|
|
41
|
+
* Internal seam — tests inject a stub MCP client factory so MCP
|
|
42
|
+
* tool resolution doesn't dial the network. Real apps leave it
|
|
43
|
+
* unset; the provider uses the default `MCPClient`.
|
|
44
|
+
*/
|
|
45
|
+
mcpClientFactory?: ResolveMcpToolsOptions['clientFactory']
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class MiniMaxBrainDriver extends OpenAICompatBrainDriver {
|
|
49
|
+
constructor(name: string, config: MiniMaxProviderConfig, options: MiniMaxProviderOptions = {}) {
|
|
50
|
+
super(
|
|
51
|
+
name,
|
|
52
|
+
{
|
|
53
|
+
driver: 'openai',
|
|
54
|
+
apiKey: config.apiKey,
|
|
55
|
+
baseUrl: config.baseUrl ?? DEFAULT_MINIMAX_BASE_URL,
|
|
56
|
+
defaultModel: config.defaultModel ?? DEFAULT_MINIMAX_MODEL,
|
|
57
|
+
...(config.defaultMaxTokens !== undefined
|
|
58
|
+
? { defaultMaxTokens: config.defaultMaxTokens }
|
|
59
|
+
: {}),
|
|
60
|
+
},
|
|
61
|
+
options,
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
override async embed(
|
|
66
|
+
_texts: readonly string[],
|
|
67
|
+
_options?: EmbedOptions,
|
|
68
|
+
): Promise<EmbedResult<OpenAI.CreateEmbeddingResponse>> {
|
|
69
|
+
throw new BrainError(
|
|
70
|
+
`MiniMaxBrainDriver.embed: MiniMax's OpenAI-compatibility endpoint does not expose embeddings. Route embed calls to a provider with native support — OpenAI / Gemini / Ollama.`,
|
|
71
|
+
{ context: { provider: this.name } },
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
override async transcribe(
|
|
76
|
+
_audio: AudioSource,
|
|
77
|
+
_options?: TranscribeOptions,
|
|
78
|
+
): Promise<TranscribeResult<OpenAI.Audio.TranscriptionCreateResponse>> {
|
|
79
|
+
throw new BrainError(
|
|
80
|
+
"MiniMaxBrainDriver.transcribe: MiniMax's OpenAI-compatibility endpoint does not expose audio transcription. Route transcribe calls to a provider with native support — OpenAI / Ollama / Gemini.",
|
|
81
|
+
{ context: { provider: this.name } },
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { OpenRouterBrainDriver } from './openrouter_brain_driver.ts'
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `OpenRouterBrainDriver` — `OpenAICompatBrainDriver` pointed at
|
|
3
|
+
* OpenRouter's OpenAI-compatible endpoint at
|
|
4
|
+
* `https://openrouter.ai/api/v1`.
|
|
5
|
+
*
|
|
6
|
+
* OpenRouter multiplexes 300+ hosted models (Anthropic, OpenAI,
|
|
7
|
+
* Google, Meta, Mistral, DeepSeek, Qwen, ...) behind one
|
|
8
|
+
* Chat-Completions-shaped API. The driver does NOT try to predict
|
|
9
|
+
* which model supports what — capabilities vary per slug. The
|
|
10
|
+
* supported approach is:
|
|
11
|
+
*
|
|
12
|
+
* - Pin an explicit model slug per call (e.g.
|
|
13
|
+
* `anthropic/claude-sonnet-4`, `meta-llama/llama-3.3-70b`).
|
|
14
|
+
* - If the chosen model doesn't support a primitive (tools,
|
|
15
|
+
* JSON mode, schema-forcing), the upstream OpenRouter error
|
|
16
|
+
* bubbles up as a `BrainError` from the inherited helpers.
|
|
17
|
+
* - Consult `https://openrouter.ai/api/v1/models` and filter on
|
|
18
|
+
* the `supported_parameters` field to pick a model that
|
|
19
|
+
* handles the feature you need.
|
|
20
|
+
*
|
|
21
|
+
* Two OpenRouter-specific niceties:
|
|
22
|
+
*
|
|
23
|
+
* - Optional `appUrl` (→ `HTTP-Referer`) and `appTitle`
|
|
24
|
+
* (→ `X-Title`) forwarded as default headers on every request.
|
|
25
|
+
* OpenRouter uses these to attribute traffic on their public
|
|
26
|
+
* leaderboard / rankings.
|
|
27
|
+
*
|
|
28
|
+
* - No `defaultModel` default — OpenRouter has no canonical pick.
|
|
29
|
+
* Apps must set one, or pass `options.model` per call. The
|
|
30
|
+
* inherited OpenAI default `gpt-5` would silently route through
|
|
31
|
+
* OpenAI's slug namespace and surprise users.
|
|
32
|
+
*
|
|
33
|
+
* Inherits all OpenAI-compat overrides: `buildParams` strips
|
|
34
|
+
* `reasoning_effort` (re-add it in a subclass / per-call `extraBody`
|
|
35
|
+
* for models that take it), `generate` uses `json_object`-mode +
|
|
36
|
+
* schema-in-system-prompt, `runWithToolsAndSchema` /
|
|
37
|
+
* `streamWithToolsAndSchema` use the tool-forcing pattern.
|
|
38
|
+
*
|
|
39
|
+
* `embed` / `transcribe` are not exposed via OpenRouter — replaced
|
|
40
|
+
* with explicit `BrainError`s.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import OpenAI from 'openai'
|
|
44
|
+
import type { OpenRouterProviderConfig } from '../../brain_config.ts'
|
|
45
|
+
import { BrainError } from '../../brain_error.ts'
|
|
46
|
+
import type { ResolveMcpToolsOptions } from '../../mcp/resolve_mcp_tools.ts'
|
|
47
|
+
import type {
|
|
48
|
+
AudioSource,
|
|
49
|
+
EmbedOptions,
|
|
50
|
+
EmbedResult,
|
|
51
|
+
TranscribeOptions,
|
|
52
|
+
TranscribeResult,
|
|
53
|
+
} from '../../types.ts'
|
|
54
|
+
import { OpenAICompatBrainDriver } from '../openai_compat/openai_compat_brain_driver.ts'
|
|
55
|
+
|
|
56
|
+
const DEFAULT_OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1'
|
|
57
|
+
/**
|
|
58
|
+
* Fallback when neither `config.defaultModel` nor a per-call
|
|
59
|
+
* `options.model` is supplied. Picked as a broad-purpose,
|
|
60
|
+
* tool-capable, well-priced default; apps that want something else
|
|
61
|
+
* just pass `defaultModel`.
|
|
62
|
+
*/
|
|
63
|
+
const DEFAULT_OPENROUTER_MODEL = 'openai/gpt-4o-mini'
|
|
64
|
+
|
|
65
|
+
export interface OpenRouterProviderOptions {
|
|
66
|
+
client?: OpenAI
|
|
67
|
+
/**
|
|
68
|
+
* Internal seam — tests inject a stub MCP client factory so MCP
|
|
69
|
+
* tool resolution doesn't dial the network. Real apps leave it
|
|
70
|
+
* unset; the provider uses the default `MCPClient`.
|
|
71
|
+
*/
|
|
72
|
+
mcpClientFactory?: ResolveMcpToolsOptions['clientFactory']
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class OpenRouterBrainDriver extends OpenAICompatBrainDriver {
|
|
76
|
+
constructor(
|
|
77
|
+
name: string,
|
|
78
|
+
config: OpenRouterProviderConfig,
|
|
79
|
+
options: OpenRouterProviderOptions = {},
|
|
80
|
+
) {
|
|
81
|
+
const baseURL = config.baseUrl ?? DEFAULT_OPENROUTER_BASE_URL
|
|
82
|
+
const defaultHeaders = buildOpenRouterHeaders(config)
|
|
83
|
+
// Construct the OpenAI client locally so we can carry OpenRouter's
|
|
84
|
+
// attribution headers (HTTP-Referer / X-Title). The base
|
|
85
|
+
// OpenAIBrainDriver only exposes apiKey / baseUrl / organization,
|
|
86
|
+
// so we hand a fully-built client through `options.client`.
|
|
87
|
+
const client =
|
|
88
|
+
options.client ??
|
|
89
|
+
new OpenAI({
|
|
90
|
+
apiKey: config.apiKey,
|
|
91
|
+
baseURL,
|
|
92
|
+
...(defaultHeaders ? { defaultHeaders } : {}),
|
|
93
|
+
})
|
|
94
|
+
super(
|
|
95
|
+
name,
|
|
96
|
+
{
|
|
97
|
+
driver: 'openai',
|
|
98
|
+
apiKey: config.apiKey,
|
|
99
|
+
baseUrl: baseURL,
|
|
100
|
+
defaultModel: config.defaultModel ?? DEFAULT_OPENROUTER_MODEL,
|
|
101
|
+
...(config.defaultMaxTokens !== undefined
|
|
102
|
+
? { defaultMaxTokens: config.defaultMaxTokens }
|
|
103
|
+
: {}),
|
|
104
|
+
},
|
|
105
|
+
{ ...options, client },
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
override async embed(
|
|
110
|
+
_texts: readonly string[],
|
|
111
|
+
_options?: EmbedOptions,
|
|
112
|
+
): Promise<EmbedResult<OpenAI.CreateEmbeddingResponse>> {
|
|
113
|
+
throw new BrainError(
|
|
114
|
+
`OpenRouterBrainDriver.embed: OpenRouter's API does not expose embeddings. Route embed calls to a provider with native support — OpenAI / Gemini / Ollama.`,
|
|
115
|
+
{ context: { provider: this.name } },
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
override async transcribe(
|
|
120
|
+
_audio: AudioSource,
|
|
121
|
+
_options?: TranscribeOptions,
|
|
122
|
+
): Promise<TranscribeResult<OpenAI.Audio.TranscriptionCreateResponse>> {
|
|
123
|
+
throw new BrainError(
|
|
124
|
+
"OpenRouterBrainDriver.transcribe: OpenRouter's API does not expose audio transcription. Route transcribe calls to a provider with native support — OpenAI / Ollama / Gemini.",
|
|
125
|
+
{ context: { provider: this.name } },
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildOpenRouterHeaders(
|
|
131
|
+
config: OpenRouterProviderConfig,
|
|
132
|
+
): Record<string, string> | undefined {
|
|
133
|
+
const headers: Record<string, string> = {}
|
|
134
|
+
if (config.appUrl !== undefined) headers['HTTP-Referer'] = config.appUrl
|
|
135
|
+
if (config.appTitle !== undefined) headers['X-Title'] = config.appTitle
|
|
136
|
+
return Object.keys(headers).length > 0 ? headers : undefined
|
|
137
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { QwenBrainDriver } from './qwen_brain_driver.ts'
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `QwenBrainDriver` — `OpenAICompatBrainDriver` pointed at Alibaba
|
|
3
|
+
* DashScope's OpenAI-compatible `/compatible-mode/v1` endpoint.
|
|
4
|
+
*
|
|
5
|
+
* Why ship Qwen: deep coverage of Chinese + SEA languages, with the
|
|
6
|
+
* `qwen-plus` / `qwen-max` line punching at the frontier on those
|
|
7
|
+
* locales. Fits Strav's SEA-first positioning.
|
|
8
|
+
*
|
|
9
|
+
* Inherits the OpenAI-compat overrides (strip `reasoning_effort`,
|
|
10
|
+
* `json_object`-mode generate with schema-in-system-prompt,
|
|
11
|
+
* tool-forcing pattern for combined tools + schema) from the base
|
|
12
|
+
* class. Only adds:
|
|
13
|
+
*
|
|
14
|
+
* - Constructor with DashScope defaults — base URL
|
|
15
|
+
* `https://dashscope-intl.aliyuncs.com/compatible-mode/v1` (the
|
|
16
|
+
* Singapore endpoint), default model `qwen-plus`.
|
|
17
|
+
*
|
|
18
|
+
* - `mapUsage` override — Qwen reports prompt-cache hits on
|
|
19
|
+
* OpenAI's standard `prompt_tokens_details.cached_tokens`
|
|
20
|
+
* field; the inherited mapping is already correct. Kept the
|
|
21
|
+
* hook here for vendor-specific extension fields if they
|
|
22
|
+
* surface later.
|
|
23
|
+
*
|
|
24
|
+
* `embed` / `transcribe` are not implemented for V1 — DashScope
|
|
25
|
+
* exposes both via separate endpoints, but the framework's seam
|
|
26
|
+
* is per-driver and we'd rather route those calls to a provider
|
|
27
|
+
* with first-class support. Override the inherited stubs to throw
|
|
28
|
+
* with a clear message instead of letting the SDK 404.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import type OpenAI from 'openai'
|
|
32
|
+
import type { QwenProviderConfig } from '../../brain_config.ts'
|
|
33
|
+
import { BrainError } from '../../brain_error.ts'
|
|
34
|
+
import type { ResolveMcpToolsOptions } from '../../mcp/resolve_mcp_tools.ts'
|
|
35
|
+
import type {
|
|
36
|
+
AudioSource,
|
|
37
|
+
EmbedOptions,
|
|
38
|
+
EmbedResult,
|
|
39
|
+
TranscribeOptions,
|
|
40
|
+
TranscribeResult,
|
|
41
|
+
} from '../../types.ts'
|
|
42
|
+
import { OpenAICompatBrainDriver } from '../openai_compat/openai_compat_brain_driver.ts'
|
|
43
|
+
|
|
44
|
+
const DEFAULT_QWEN_MODEL = 'qwen-plus'
|
|
45
|
+
const DEFAULT_QWEN_BASE_URL = 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1'
|
|
46
|
+
|
|
47
|
+
export interface QwenProviderOptions {
|
|
48
|
+
client?: OpenAI
|
|
49
|
+
/**
|
|
50
|
+
* Internal seam — tests inject a stub MCP client factory so MCP
|
|
51
|
+
* tool resolution doesn't dial the network. Real apps leave it
|
|
52
|
+
* unset; the provider uses the default `MCPClient`.
|
|
53
|
+
*/
|
|
54
|
+
mcpClientFactory?: ResolveMcpToolsOptions['clientFactory']
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class QwenBrainDriver extends OpenAICompatBrainDriver {
|
|
58
|
+
constructor(name: string, config: QwenProviderConfig, options: QwenProviderOptions = {}) {
|
|
59
|
+
super(
|
|
60
|
+
name,
|
|
61
|
+
{
|
|
62
|
+
driver: 'openai',
|
|
63
|
+
apiKey: config.apiKey,
|
|
64
|
+
baseUrl: config.baseUrl ?? DEFAULT_QWEN_BASE_URL,
|
|
65
|
+
defaultModel: config.defaultModel ?? DEFAULT_QWEN_MODEL,
|
|
66
|
+
...(config.defaultMaxTokens !== undefined
|
|
67
|
+
? { defaultMaxTokens: config.defaultMaxTokens }
|
|
68
|
+
: {}),
|
|
69
|
+
},
|
|
70
|
+
options,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* DashScope's compatibility endpoint does not expose embeddings
|
|
76
|
+
* via `/compatible-mode/v1/embeddings`. Override the inherited
|
|
77
|
+
* `embed` to throw clearly rather than 404 at the wire.
|
|
78
|
+
*/
|
|
79
|
+
override async embed(
|
|
80
|
+
_texts: readonly string[],
|
|
81
|
+
_options?: EmbedOptions,
|
|
82
|
+
): Promise<EmbedResult<OpenAI.CreateEmbeddingResponse>> {
|
|
83
|
+
throw new BrainError(
|
|
84
|
+
`QwenBrainDriver.embed: Qwen's OpenAI-compatibility endpoint does not expose embeddings. Route embed calls to a provider with native support — OpenAI / Gemini / Ollama.`,
|
|
85
|
+
{ context: { provider: this.name } },
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* DashScope's compatibility endpoint does not expose audio
|
|
91
|
+
* transcription. Override the inherited `transcribe` to throw
|
|
92
|
+
* clearly.
|
|
93
|
+
*/
|
|
94
|
+
override async transcribe(
|
|
95
|
+
_audio: AudioSource,
|
|
96
|
+
_options?: TranscribeOptions,
|
|
97
|
+
): Promise<TranscribeResult<OpenAI.Audio.TranscriptionCreateResponse>> {
|
|
98
|
+
throw new BrainError(
|
|
99
|
+
"QwenBrainDriver.transcribe: Qwen's OpenAI-compatibility endpoint does not expose audio transcription. Route transcribe calls to a provider with native support — OpenAI / Ollama / Gemini.",
|
|
100
|
+
{ context: { provider: this.name } },
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Shipped:
|
|
4
4
|
// - `BrainDriver` contract + concrete drivers — Anthropic, OpenAI
|
|
5
|
-
// (Chat + Responses), Gemini, DeepSeek, Ollama,
|
|
5
|
+
// (Chat + Responses), Gemini, DeepSeek, Ollama, Qwen, MiniMax,
|
|
6
|
+
// OpenRouter, openai-compat.
|
|
6
7
|
// - `BrainManager` + `Thread` (persisted history, compaction) + the
|
|
7
8
|
// `BrainProvider` service wiring + prompt-cache plumbing.
|
|
8
9
|
// - Tools — `defineTool`, `Agent` base + `AgentRunner`,
|
|
@@ -17,8 +18,8 @@ export { Agent } from './agent.ts'
|
|
|
17
18
|
export type { AgentGenerateResult } from './agent_generate_result.ts'
|
|
18
19
|
export type { AgentResult } from './agent_result.ts'
|
|
19
20
|
export {
|
|
20
|
-
AgentRunner,
|
|
21
21
|
type AgentRunMaybeSuspended,
|
|
22
|
+
AgentRunner,
|
|
22
23
|
type AgentRunResult,
|
|
23
24
|
} from './agent_runner.ts'
|
|
24
25
|
export type { AgentStreamEvent } from './agent_stream_event.ts'
|
|
@@ -26,15 +27,23 @@ export {
|
|
|
26
27
|
type AnthropicProviderConfig,
|
|
27
28
|
type BrainCacheConfig,
|
|
28
29
|
type BrainConfigShape,
|
|
29
|
-
type DeepSeekProviderConfig,
|
|
30
30
|
DEFAULT_MODEL,
|
|
31
31
|
DEFAULT_TIERS,
|
|
32
|
+
type DeepSeekProviderConfig,
|
|
32
33
|
type GeminiProviderConfig,
|
|
34
|
+
type MiniMaxProviderConfig,
|
|
33
35
|
type OllamaProviderConfig,
|
|
34
36
|
type OpenAIProviderConfig,
|
|
35
37
|
type OpenAIResponsesProviderConfig,
|
|
38
|
+
type OpenRouterProviderConfig,
|
|
36
39
|
type ProviderConfig,
|
|
40
|
+
type QwenProviderConfig,
|
|
37
41
|
} from './brain_config.ts'
|
|
42
|
+
export type {
|
|
43
|
+
BrainDriver,
|
|
44
|
+
RunWithToolsOptions,
|
|
45
|
+
RunWithToolsOptionsWithSuspend,
|
|
46
|
+
} from './brain_driver.ts'
|
|
38
47
|
export {
|
|
39
48
|
BrainConfigError,
|
|
40
49
|
BrainError,
|
|
@@ -48,22 +57,20 @@ export {
|
|
|
48
57
|
type BrainManagerOptions,
|
|
49
58
|
} from './brain_manager.ts'
|
|
50
59
|
export { BrainProvider } from './brain_provider.ts'
|
|
51
|
-
export {
|
|
52
|
-
export { MCPClientPool, type MCPClientFactory } from './mcp/pool.ts'
|
|
53
|
-
export type { MCPServer, MCPServerToolConfig } from './mcp_server.ts'
|
|
54
|
-
export type { OutputSchema } from './output_schema.ts'
|
|
60
|
+
export { type DefineToolSpec, defineTool } from './define_tool.ts'
|
|
55
61
|
export { AnthropicBrainDriver } from './drivers/anthropic/anthropic_brain_driver.ts'
|
|
56
62
|
export { DeepSeekBrainDriver } from './drivers/deepseek/deepseek_brain_driver.ts'
|
|
57
63
|
export { GeminiBrainDriver } from './drivers/gemini/gemini_brain_driver.ts'
|
|
64
|
+
export { MiniMaxBrainDriver } from './drivers/minimax/minimax_brain_driver.ts'
|
|
58
65
|
export { OllamaBrainDriver } from './drivers/ollama/ollama_brain_driver.ts'
|
|
59
|
-
export { OpenAICompatBrainDriver } from './drivers/openai_compat/openai_compat_brain_driver.ts'
|
|
60
66
|
export { OpenAIBrainDriver } from './drivers/openai/openai_brain_driver.ts'
|
|
67
|
+
export { OpenAICompatBrainDriver } from './drivers/openai_compat/openai_compat_brain_driver.ts'
|
|
61
68
|
export { OpenAIResponsesBrainDriver } from './drivers/openai_responses/openai_responses_brain_driver.ts'
|
|
62
|
-
export
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
} from './
|
|
69
|
+
export { OpenRouterBrainDriver } from './drivers/openrouter/openrouter_brain_driver.ts'
|
|
70
|
+
export { QwenBrainDriver } from './drivers/qwen/qwen_brain_driver.ts'
|
|
71
|
+
export { type MCPClientFactory, MCPClientPool } from './mcp/pool.ts'
|
|
72
|
+
export type { MCPServer, MCPServerToolConfig } from './mcp_server.ts'
|
|
73
|
+
export type { OutputSchema } from './output_schema.ts'
|
|
67
74
|
export {
|
|
68
75
|
appendResumeResults,
|
|
69
76
|
isSuspended,
|
|
@@ -75,14 +82,14 @@ export { Thread, type ThreadOptions, type ThreadState } from './thread.ts'
|
|
|
75
82
|
export type { Tool, ToolContext } from './tool.ts'
|
|
76
83
|
export { ToolExecutionError } from './tool_execution_error.ts'
|
|
77
84
|
export type {
|
|
85
|
+
AudioBlock,
|
|
86
|
+
AudioSource,
|
|
78
87
|
ChatOptions,
|
|
79
88
|
ChatResult,
|
|
80
89
|
ChatUsage,
|
|
81
90
|
CompactConfig,
|
|
82
91
|
CompactionBlock,
|
|
83
92
|
ContentBlock,
|
|
84
|
-
AudioBlock,
|
|
85
|
-
AudioSource,
|
|
86
93
|
DocumentBlock,
|
|
87
94
|
EmbedOptions,
|
|
88
95
|
EmbedResult,
|