@jungtz/chat-kit 0.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 +162 -0
- package/dist/engine/engine.d.ts +18 -0
- package/dist/engine/types.d.ts +133 -0
- package/dist/guards/input.d.ts +16 -0
- package/dist/guards/output.d.ts +45 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -0
- package/dist/models/create-model.d.ts +21 -0
- package/dist/models/fallback-model.d.ts +25 -0
- package/dist/models/from-config.d.ts +55 -0
- package/dist/models/index.d.ts +19 -0
- package/dist/models/index.js +1 -0
- package/dist/models/index.js.map +1 -0
- package/dist/models/key-ring.d.ts +70 -0
- package/dist/models/list-models.d.ts +30 -0
- package/dist/models/model-spec.d.ts +34 -0
- package/dist/models/protocol.d.ts +33 -0
- package/dist/models/quirks.d.ts +54 -0
- package/dist/models/routed-model.d.ts +43 -0
- package/dist/models/stream-peek.d.ts +27 -0
- package/dist/models/types.d.ts +51 -0
- package/dist/prompt/assemble.d.ts +39 -0
- package/dist/prompt/markers.d.ts +20 -0
- package/dist/prompt/types.d.ts +55 -0
- package/docs/API.md +153 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jungtz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# @jungtz/chat-kit
|
|
2
|
+
|
|
3
|
+
通用 Chat 引擎,以 [Vercel AI SDK](https://ai-sdk.dev)(`ai@6`)為模型層。
|
|
4
|
+
|
|
5
|
+
| 模組 | 內容 | 狀態 |
|
|
6
|
+
| :-- | :-- | :-- |
|
|
7
|
+
| `models/` | provider 設定 → `LanguageModel`:多金鑰輪替、主備援、provider 特例 | ✅ P1 |
|
|
8
|
+
| `engine/` | `runTurn`(串流事件)/`completeTurn`(完整結果)、工具迴圈 | ✅ P2 |
|
|
9
|
+
| `prompt/` | 片段+上下文區塊組裝、固定順序、信任標記、佔位符檢查 | ✅ P2 |
|
|
10
|
+
| `guards/` | 輸入標記中和、輸出標記清洗、模型自我介紹過濾、自訂閘門介面 | ✅ P2 |
|
|
11
|
+
| `knowledge/` | wiki-router 整合 | ⛔ 不建(P5 決定,見 PLAN §6.1) |
|
|
12
|
+
|
|
13
|
+
完整計畫見 `docs/CHAT_KIT_PLAN.md`,待執行事項見 `docs/NEXT_STEPS.md`,從發問到輸出的完整流程與接入範例見 `docs/FLOW.md`,介面簽名速查見 `docs/API.md`(隨 npm 包出貨)。
|
|
14
|
+
|
|
15
|
+
## 安裝
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @jungtz/chat-kit ai@^6 @ai-sdk/provider@^3
|
|
19
|
+
# 依用到的協定擇一或多個安裝(皆為 optional peer)
|
|
20
|
+
npm install @ai-sdk/openai-compatible@^2 # /chat/completions
|
|
21
|
+
npm install @ai-sdk/openai@^3 # /responses
|
|
22
|
+
npm install @ai-sdk/anthropic@^3 # /messages
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- **`ai` 必須釘 `^6`**(`@ai-sdk/provider` 釘 `^3`):引擎以 `LanguageModelV3` 介面為準,`ai@7` 的變動未支援。
|
|
26
|
+
- npm 包只含 `dist/`(壓縮後的 ESM+`.d.ts`)、`README.md`、`docs/API.md`、`LICENSE`;其餘文件在 repo 的 `docs/`。
|
|
27
|
+
|
|
28
|
+
## 用法
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
import { streamText } from 'ai'
|
|
32
|
+
import { createModelFromConfig, reasoningOptions } from '@jungtz/chat-kit/models'
|
|
33
|
+
|
|
34
|
+
// providers 形狀見下方〈設定形狀〉
|
|
35
|
+
const { model, targets } = createModelFromConfig(config.providers, config.premium.chat, {
|
|
36
|
+
requireApiKey: true,
|
|
37
|
+
routed: { identity: { userAgent: 'my-app/1.0.0', sessionId: crypto.randomUUID() } },
|
|
38
|
+
fallback: { onFallback: ({ to }) => console.warn('換手至', to.modelId) },
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const result = streamText({
|
|
42
|
+
model,
|
|
43
|
+
messages,
|
|
44
|
+
maxRetries: 0, // 輪替已在 chat-kit 處理,別讓 SDK 再疊一層重試
|
|
45
|
+
providerOptions: reasoningOptions('low'), // 本次覆寫推理強度;省略則用設定值
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
不用 `config.json` 的專案可以完全不碰 `models/`:引擎只認 AI SDK 的 `LanguageModel`,
|
|
50
|
+
直接傳 `google('gemini-…')` 之類的 model 即可。
|
|
51
|
+
|
|
52
|
+
### 引擎
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
import { createChatEngine, selfIntroGuard } from '@jungtz/chat-kit'
|
|
56
|
+
|
|
57
|
+
const engine = createChatEngine({
|
|
58
|
+
domains: {
|
|
59
|
+
demo: {
|
|
60
|
+
id: 'demo',
|
|
61
|
+
prompt: { parts: [identityMd, rulesMd], channelParts: { line: [lineRulesMd] }, placeholders: { NAME: '小幫手' } },
|
|
62
|
+
// 回傳順序不影響結果:引擎依 order、key 排序後才拼接
|
|
63
|
+
buildContext: async (turn) => [
|
|
64
|
+
{ key: 'profile', order: 10, trust: 'reference', placement: 'system', title: '基本資料', content: await loadProfile(turn.entity) },
|
|
65
|
+
{ key: 'live', order: 20, trust: 'realtime', placement: 'user', title: '即時狀態', content: await loadLive(turn.entity) },
|
|
66
|
+
],
|
|
67
|
+
tools: (turn) => ({ /* AI SDK tool(),含 execute */ }),
|
|
68
|
+
outputGuards: [selfIntroGuard(targets.map((t) => t.model))],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// 串流(Web)
|
|
74
|
+
for await (const ev of engine.runTurn({ domain: 'demo', channel: 'web', model, messages, entity })) {
|
|
75
|
+
if (ev.type === 'content') res.write(`data: ${JSON.stringify({ type: 'content', content: ev.text })}
|
|
76
|
+
|
|
77
|
+
`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 一次拿完整結果(LINE、批次)
|
|
81
|
+
const { text, usage, toolCalls, guards } = await engine.completeTurn({ domain: 'demo', channel: 'line', model, messages })
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
事件:`context`(實際注入的區塊 key)、`thinking`、`content`、`tool-call`、`tool-result`、`guard`、
|
|
85
|
+
`usage`、`done`(最終文字;若有閘門改寫,`rewritten: true`)、`error`。**出錯時不送 `done`**——半截內容不得被當成成功結果。
|
|
86
|
+
|
|
87
|
+
區塊預設以信任標記包住(`[REFERENCE: 標題] … [END REFERENCE]`);使用者訊息裡偽造的同名標記會被換成全形括號。
|
|
88
|
+
既有系統若區塊自帶 markdown 標題、且尚未驗證改格式後的行為,可用 `format: 'raw'` 原樣輸出。
|
|
89
|
+
|
|
90
|
+
## 設定形狀
|
|
91
|
+
|
|
92
|
+
```jsonc
|
|
93
|
+
{
|
|
94
|
+
"providers": {
|
|
95
|
+
"main": {
|
|
96
|
+
"baseURL": "https://llm.example.com/v1",
|
|
97
|
+
"apiKey": "${LLM_API_KEY}", // 支援 ${ENV} 展開
|
|
98
|
+
"apiKeys": ["sk-a", "sk-b"], // 與 apiKey 合併去重,429 時輪替
|
|
99
|
+
"defaultModel": "example-chat-1",
|
|
100
|
+
"reasoningEffort": "high", // 選填
|
|
101
|
+
"sdk": { "*": "openai-compatible", "example-reasoner-1": "openai" }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
| `sdk` 值 | 端點 | 套件 |
|
|
108
|
+
| :-- | :-- | :-- |
|
|
109
|
+
| `openai-compatible`(預設,可省略) | `POST /chat/completions` | `@ai-sdk/openai-compatible` |
|
|
110
|
+
| `openai` | `POST /responses` | `@ai-sdk/openai` |
|
|
111
|
+
| `anthropic` | `POST /messages`(`x-api-key`) | `@ai-sdk/anthropic` |
|
|
112
|
+
|
|
113
|
+
**只登記「走預設會失敗」的模型**。舊值域 `chat` / `responses` / `messages` 仍可用。
|
|
114
|
+
|
|
115
|
+
## 推理強度
|
|
116
|
+
|
|
117
|
+
| 值 | 送出 |
|
|
118
|
+
| :-- | :-- |
|
|
119
|
+
| 未設定 | 不帶參數(模型自身預設;部分推理模型預設**開**思考) |
|
|
120
|
+
| `'none'` | 照實送出 `reasoning_effort: "none"`,明確關閉思考 |
|
|
121
|
+
| `'low'` / `'high'` … | 照實送出 |
|
|
122
|
+
| `reasoningOptions(null)` | 本次明確不帶,即使設定有預設值 |
|
|
123
|
+
|
|
124
|
+
provider 特例在 `src/models/quirks.ts` 一處處理:Groq 不帶、Agnes 改用
|
|
125
|
+
`chat_template_kwargs.enable_thinking`、`/responses` 加 `forceReasoning`(否則 AI SDK
|
|
126
|
+
會對它不認得的模型靜默丟掉 effort)、Anthropic 不帶。
|
|
127
|
+
|
|
128
|
+
## 換手規則
|
|
129
|
+
|
|
130
|
+
| 情況 | 金鑰輪替(同 provider) | 主備援(跨 provider) |
|
|
131
|
+
| :-- | :-- | :-- |
|
|
132
|
+
| 429 / 配額錯誤,尚無輸出 | ✅ 冷卻並換下一把(遵守 `Retry-After`) | ✅ |
|
|
133
|
+
| 其他錯誤,尚無輸出 | ❌ 原樣拋出 | ✅ |
|
|
134
|
+
| 只輸出了 thinking | ❌ | ✅ |
|
|
135
|
+
| 已輸出正文 | ❌ | ❌(避免兩個模型的回覆拼接) |
|
|
136
|
+
|
|
137
|
+
## 常見陷阱
|
|
138
|
+
|
|
139
|
+
- **`ai` 釘 `^6`**:`ai@7` 的介面變動未支援,混裝會在型別或執行期爆掉。
|
|
140
|
+
- **自己呼叫 `streamText` 時設 `maxRetries: 0`**:輪替與備援已在 models 層,SDK 重試會再疊一層。
|
|
141
|
+
- **成功判定看 `done`**:出錯不送 `done`,「串流結束了」不代表成功。
|
|
142
|
+
- **usage 缺席記 `null` 不記 `0`**:`0` 是「真的消耗 0」,會污染統計。
|
|
143
|
+
|
|
144
|
+
完整清單(providerOptions 命名空間不得含點、messages 驗證、依賴釘版等)見 `docs/API.md` §6。
|
|
145
|
+
|
|
146
|
+
## 開發
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
npm test # 單元測試(不打網路)
|
|
150
|
+
npm run typecheck
|
|
151
|
+
npm run build
|
|
152
|
+
node scripts/smoke-live.mjs # 實打各 provider 一次(消耗配額)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## 版本紀律
|
|
156
|
+
|
|
157
|
+
本機就是 production:使用端**禁止**以 `file:` 或 `npm link` 依賴本套件——pm2 重啟時會直接吃到
|
|
158
|
+
尚未提交的改動。一律釘死版本(npm 版本號或 git tag)。
|
|
159
|
+
|
|
160
|
+
發布走 GitHub Actions(`.github/workflows/release.yml`,同 wiki-router):push 到 master 且動到
|
|
161
|
+
發布內容(`src/`、rollup 設定、`README.md`、`docs/API.md` 等)時自動升版發布;只改測試、
|
|
162
|
+
內部文件或 workflow 不會發版。tag 一律在 npm publish 成功後才建立。
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 單輪對話引擎。
|
|
3
|
+
*
|
|
4
|
+
* 流程:① 輸入閘門 → ② Domain 上下文 → ③ 組提示詞 → ④ 模型(含工具迴圈)→ ⑤ 輸出閘門
|
|
5
|
+
*
|
|
6
|
+
* 引擎不碰 HTTP、不寫 DB、不讀設定:輸出是事件序列,由各通道自行轉成 SSE、LINE 訊息或
|
|
7
|
+
* OpenAI chunk;用量紀錄與對話持久化都是呼叫端的事。
|
|
8
|
+
*/
|
|
9
|
+
import type { ChatEvent, DomainAdapter, TurnInput, TurnResult } from './types.js';
|
|
10
|
+
export declare const DEFAULT_MAX_TOOL_STEPS = 3;
|
|
11
|
+
export interface ChatEngineOptions {
|
|
12
|
+
domains: Readonly<Record<string, DomainAdapter>>;
|
|
13
|
+
}
|
|
14
|
+
export interface ChatEngine {
|
|
15
|
+
runTurn(input: TurnInput): AsyncGenerator<ChatEvent, void, undefined>;
|
|
16
|
+
completeTurn(input: TurnInput): Promise<TurnResult>;
|
|
17
|
+
}
|
|
18
|
+
export declare function createChatEngine(options: ChatEngineOptions): ChatEngine;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 引擎型別。
|
|
3
|
+
*/
|
|
4
|
+
import type { LanguageModel, PrepareStepFunction, ToolSet } from 'ai';
|
|
5
|
+
import type { SharedV3ProviderOptions as ProviderOptions } from '@ai-sdk/provider';
|
|
6
|
+
import type { ContextBlock, PromptSpec } from '../prompt/types.js';
|
|
7
|
+
import type { OutputGuard } from '../guards/output.js';
|
|
8
|
+
export interface ChatMessage {
|
|
9
|
+
role: 'system' | 'user' | 'assistant';
|
|
10
|
+
content: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 一輪對話的輸入。
|
|
14
|
+
*/
|
|
15
|
+
export interface TurnInput {
|
|
16
|
+
/** 要用哪個已註冊的 Domain */
|
|
17
|
+
domain: string;
|
|
18
|
+
channel: string;
|
|
19
|
+
/** 多租戶時的租戶 ID,原樣傳給 Domain */
|
|
20
|
+
tenantId?: string;
|
|
21
|
+
/** 對話標的(例如某個商品、某個帳戶),原樣傳給 Domain */
|
|
22
|
+
entity?: Readonly<Record<string, unknown>>;
|
|
23
|
+
/** AI SDK LanguageModel;可以是 chat-kit `models/` 產出的路由/主備援 model */
|
|
24
|
+
model: LanguageModel;
|
|
25
|
+
/**
|
|
26
|
+
* 對話歷史,最後一則必須是 user。
|
|
27
|
+
* system 訊息會接在 Domain 片段之後(供既有系統把前端組好的 system prompt 原樣帶入)。
|
|
28
|
+
*/
|
|
29
|
+
messages: readonly ChatMessage[];
|
|
30
|
+
modelOptions?: {
|
|
31
|
+
temperature?: number;
|
|
32
|
+
maxOutputTokens?: number;
|
|
33
|
+
providerOptions?: ProviderOptions;
|
|
34
|
+
/** 逾時;`chunkMs` 是串流閒置上限 */
|
|
35
|
+
timeout?: number | {
|
|
36
|
+
totalMs?: number;
|
|
37
|
+
stepMs?: number;
|
|
38
|
+
chunkMs?: number;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
signal?: AbortSignal;
|
|
42
|
+
}
|
|
43
|
+
/** Domain 看得到的這一輪資訊(不含 model,Domain 不該依模型改變資料) */
|
|
44
|
+
export interface TurnContext {
|
|
45
|
+
domain: string;
|
|
46
|
+
channel: string;
|
|
47
|
+
tenantId?: string;
|
|
48
|
+
entity?: Readonly<Record<string, unknown>>;
|
|
49
|
+
/** 已經過輸入閘門的訊息 */
|
|
50
|
+
messages: readonly ChatMessage[];
|
|
51
|
+
/** 最後一則使用者訊息原文(已中和標記) */
|
|
52
|
+
userMessage: string;
|
|
53
|
+
signal?: AbortSignal;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 領域轉接器:Domain 提供資料、工具、提示詞,引擎負責其餘一切。
|
|
57
|
+
*/
|
|
58
|
+
export interface DomainAdapter {
|
|
59
|
+
id: string;
|
|
60
|
+
prompt: PromptSpec | ((turn: TurnContext) => PromptSpec | Promise<PromptSpec>);
|
|
61
|
+
/**
|
|
62
|
+
* 本輪的上下文區塊。可平行抓取,回傳順序不影響結果(引擎依 order/key 排序)。
|
|
63
|
+
* 個別資料源失敗應自行降級成空 content,不要讓整輪失敗。
|
|
64
|
+
*/
|
|
65
|
+
buildContext?(turn: TurnContext): Promise<ContextBlock[]> | ContextBlock[];
|
|
66
|
+
/** AI SDK 的 tools(含 execute) */
|
|
67
|
+
tools?(turn: TurnContext): ToolSet | Promise<ToolSet>;
|
|
68
|
+
/** 工具最多跑幾輪(不含最後的回答輪),預設 3 */
|
|
69
|
+
maxToolSteps?: number;
|
|
70
|
+
/** 每一步呼叫前的鉤子,例如偵測到未經查證的斷言時強制重查 */
|
|
71
|
+
prepareStep?: PrepareStepFunction<ToolSet>;
|
|
72
|
+
/** 領域專屬輸出閘門,接在內建閘門之後 */
|
|
73
|
+
outputGuards?: readonly OutputGuard[];
|
|
74
|
+
}
|
|
75
|
+
export interface TurnUsage {
|
|
76
|
+
inputTokens: number | undefined;
|
|
77
|
+
outputTokens: number | undefined;
|
|
78
|
+
reasoningTokens: number | undefined;
|
|
79
|
+
totalTokens: number | undefined;
|
|
80
|
+
}
|
|
81
|
+
export type ChatEvent = {
|
|
82
|
+
type: 'context';
|
|
83
|
+
injectedKeys: string[];
|
|
84
|
+
} | {
|
|
85
|
+
type: 'thinking';
|
|
86
|
+
text: string;
|
|
87
|
+
} | {
|
|
88
|
+
type: 'content';
|
|
89
|
+
text: string;
|
|
90
|
+
} | {
|
|
91
|
+
type: 'tool-call';
|
|
92
|
+
toolName: string;
|
|
93
|
+
input: unknown;
|
|
94
|
+
} | {
|
|
95
|
+
type: 'tool-result';
|
|
96
|
+
toolName: string;
|
|
97
|
+
output: unknown;
|
|
98
|
+
} | {
|
|
99
|
+
type: 'guard';
|
|
100
|
+
name: string;
|
|
101
|
+
action: 'neutralized' | 'transformed' | 'rewrite' | 'observed';
|
|
102
|
+
detail?: string;
|
|
103
|
+
} | {
|
|
104
|
+
type: 'usage';
|
|
105
|
+
usage: TurnUsage;
|
|
106
|
+
} | {
|
|
107
|
+
type: 'done';
|
|
108
|
+
/** 套用全部閘門後的最終文字;若有 rewrite,與串流送出的內容不同 */
|
|
109
|
+
text: string;
|
|
110
|
+
rewritten: boolean;
|
|
111
|
+
finishReason: string;
|
|
112
|
+
} | {
|
|
113
|
+
type: 'error';
|
|
114
|
+
error: unknown;
|
|
115
|
+
};
|
|
116
|
+
export interface TurnResult {
|
|
117
|
+
text: string;
|
|
118
|
+
thinking: string;
|
|
119
|
+
rewritten: boolean;
|
|
120
|
+
finishReason: string;
|
|
121
|
+
usage: TurnUsage | null;
|
|
122
|
+
toolCalls: Array<{
|
|
123
|
+
toolName: string;
|
|
124
|
+
input: unknown;
|
|
125
|
+
output?: unknown;
|
|
126
|
+
}>;
|
|
127
|
+
guards: Array<{
|
|
128
|
+
name: string;
|
|
129
|
+
action: string;
|
|
130
|
+
detail?: string;
|
|
131
|
+
}>;
|
|
132
|
+
injectedKeys: string[];
|
|
133
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 輸入閘門:中和使用者訊息裡偽造的信任標記。
|
|
3
|
+
*
|
|
4
|
+
* 做法是**換括號、保留內容**(`[END REFERENCE]` → `[END REFERENCE]`),不是刪除:
|
|
5
|
+
* 使用者可能只是在句子裡剛好提到這些字,整段刪掉會讓語意殘缺;換成全形括號後保留可讀性,
|
|
6
|
+
* 同時讓它不再具備標記的語法效力——模型看到的是一段被引述的文字。
|
|
7
|
+
*
|
|
8
|
+
* 只處理本系統實際使用的標記(見 prompt/markers.ts)。這**不是**通用的 prompt-injection
|
|
9
|
+
* 過濾器:那種東西擋不完,也會誤傷正常訊息;「這句話是不是在下指令」屬於語意判斷,歸提示詞。
|
|
10
|
+
*/
|
|
11
|
+
export interface NeutralizeResult {
|
|
12
|
+
text: string;
|
|
13
|
+
/** 被中和的標記數;> 0 代表有人嘗試偽造(或剛好打到),值得記錄 */
|
|
14
|
+
neutralized: number;
|
|
15
|
+
}
|
|
16
|
+
export declare function neutralizeMarkers(text: string): NeutralizeResult;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 輸出閘門。
|
|
3
|
+
*
|
|
4
|
+
* 兩種形態,對應串流的兩個時間點:
|
|
5
|
+
* - `transform`:**串流中**對累積緩衝區做的決定性清洗(例如去掉模型吐出的內部標記),
|
|
6
|
+
* 結果以 delta 送出。必須是純函式,同一段輸入永遠得到同一段輸出。
|
|
7
|
+
* - `check`:**串流結束後**對完整回覆做的判定,可改寫最終文字。串流通道已送出的內容收不回來,
|
|
8
|
+
* 所以改寫結果只出現在 `done.text`,由通道自行決定要不要整段替換顯示。
|
|
9
|
+
*
|
|
10
|
+
* 閘門觸發一律回報(`guard` 事件):命中率長期是 0% 或 100%,都代表規則本身有問題。
|
|
11
|
+
*/
|
|
12
|
+
export interface GuardContext {
|
|
13
|
+
/** 本輪使用的模型 ID(主備援時為主模型) */
|
|
14
|
+
modelId: string;
|
|
15
|
+
channel: string;
|
|
16
|
+
}
|
|
17
|
+
export interface GuardVerdict {
|
|
18
|
+
action: 'pass' | 'rewrite';
|
|
19
|
+
/** `rewrite` 時的新文字 */
|
|
20
|
+
text?: string;
|
|
21
|
+
/**
|
|
22
|
+
* 給日誌看的原因。
|
|
23
|
+
* `rewrite` 時隨 `guard` 事件送出;**`pass` 帶了 reason 則是觀察模式**——放行、不改回覆,
|
|
24
|
+
* 但引擎仍送出 `guard` 事件(`action: 'observed'`),供規則上線前量測命中率。
|
|
25
|
+
*/
|
|
26
|
+
reason?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface OutputGuard {
|
|
29
|
+
name: string;
|
|
30
|
+
transform?(buffer: string, ctx: GuardContext): string;
|
|
31
|
+
check?(finalText: string, ctx: GuardContext): GuardVerdict | Promise<GuardVerdict>;
|
|
32
|
+
}
|
|
33
|
+
/** 清掉模型吐出的內部信任標記(只去標記本身,保留其間文字) */
|
|
34
|
+
export declare const stripMarkersGuard: OutputGuard;
|
|
35
|
+
/**
|
|
36
|
+
* 模型自我介紹/名稱洩漏過濾。移植自 stock-viewer `server/utils/chat-sanitizer.js`,規則不變。
|
|
37
|
+
*
|
|
38
|
+
* ⚠️ 會連帶移除正文中出現的模型系列名稱(gpt、claude、gemini…),所以是**選用**閘門:
|
|
39
|
+
* 討論 AI 產品本身的 Domain 不該開。
|
|
40
|
+
*
|
|
41
|
+
* @param modelNames 本輪可能出場的模型名稱(含備援),全部都要精確比對
|
|
42
|
+
*/
|
|
43
|
+
export declare function selfIntroGuard(modelNames: readonly string[]): OutputGuard;
|
|
44
|
+
/** 依序套用所有 transform */
|
|
45
|
+
export declare function applyTransforms(buffer: string, guards: readonly OutputGuard[], ctx: GuardContext): string;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type { ChatEvent, ChatMessage, DomainAdapter, TurnContext, TurnInput, TurnResult, TurnUsage, } from './engine/types.js';
|
|
2
|
+
export type { ChatEngine, ChatEngineOptions } from './engine/engine.js';
|
|
3
|
+
export { DEFAULT_MAX_TOOL_STEPS, createChatEngine } from './engine/engine.js';
|
|
4
|
+
export type { Channel, ContextBlock, PromptSpec, Trust } from './prompt/types.js';
|
|
5
|
+
export type { AssembleInput, AssembledPrompt } from './prompt/assemble.js';
|
|
6
|
+
export { assemblePrompt, expandPlaceholders, orderBlocks, renderBlock } from './prompt/assemble.js';
|
|
7
|
+
export { TRUST_LABELS, closeMarker, markerPattern, openMarker } from './prompt/markers.js';
|
|
8
|
+
export type { NeutralizeResult } from './guards/input.js';
|
|
9
|
+
export { neutralizeMarkers } from './guards/input.js';
|
|
10
|
+
export type { GuardContext, GuardVerdict, OutputGuard } from './guards/output.js';
|
|
11
|
+
export { applyTransforms, selfIntroGuard, stripMarkersGuard } from './guards/output.js';
|
|
12
|
+
export * from './models/index.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{streamText as e,stepCountIs as t}from"ai";export{CHAT_KIT_OPTIONS_KEY,DEFAULT_ANTHROPIC_MAX_OUTPUT_TOKENS,DEFAULT_COOLDOWN_MS,DEFAULT_PROTOCOL,MAX_COOLDOWN_MS,PROTOCOL_ALIASES,consoleLogger,createFallbackModel,createKeyRing,createLanguageModel,createModelFromConfig,createRoutedModel,defaultKeyRing,isOpenCodeEndpoint,isQuotaError,listModels,maskApiKey,normalizeApiKeyList,normalizeBaseURL,normalizeModelConfig,normalizeProtocol,openCodeHeaders,parseModelSpec,reasoningOptions,reasoningProviderOptions,resolveModelChain,resolveModelTarget,resolveProtocol,resolveProviderApiKeys,retryAfterMsFromError}from"./models/index.js";const n={authoritative:"AUTHORITATIVE",reference:"REFERENCE",realtime:"REALTIME"};function o(e,t){return`[${n[e]}: ${t}]`}function r(e){return`[END ${n[e]}]`}function s(){const e=Object.values(n).join("|");return new RegExp(`\\[\\s*((?:END\\s+)?(?:${e})\\b[^\\]\\n]*)\\]`,"gi")}const a=/\{\{\s*([A-Za-z0-9_]+)\s*\}\}/g;function i(e,t={}){const n=new Set,o=e.replace(a,(e,o)=>Object.prototype.hasOwnProperty.call(t,o)?t[o]:(n.add(o),e));if(n.size>0)throw new Error(`[chat-kit/prompt] 提示詞有未展開的佔位符:${[...n].join(", ")}`);return o}function l(e){const t=new Set;for(const n of e){if(t.has(n.key))throw new Error(`[chat-kit/prompt] 上下文區塊 key 重複:${n.key}`);t.add(n.key)}return e.filter(e=>e.content.trim().length>0).slice().sort((e,t)=>e.order-t.order||(e.key<t.key?-1:e.key>t.key?1:0))}function c(e){return"raw"===e.format?e.content:`${o(e.trust,e.title)}\n${e.content}\n${r(e.trust)}`}const u=({blocks:e,message:t})=>e?`${e}\n\n${t}`:t;function p(e){const{spec:t,channel:n,userMessage:o}=e,r=l(e.blocks),s=r.filter(e=>"system"===e.placement),a=r.filter(e=>"user"===e.placement);return{system:[...[...t.parts,...t.channelParts?.[n]??[]].map(e=>i(e,t.placeholders)),...e.extraSystem??[],...s.map(c)].filter(e=>e.trim().length>0).join("\n\n"),userMessage:(t.composeUserMessage??u)({blocks:a.map(c).join("\n\n"),message:o}),injectedKeys:r.map(e=>e.key)}}function m(e){let t=0;return{text:e.replace(s(),(e,n)=>(t++,`[${n}]`)),neutralized:t}}const d={name:"strip-markers",transform:e=>e.replace(s(),"")};function g(e){const t=[];for(const n of e)n&&n.trim()&&t.push(new RegExp(n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"gi"));return t.push(/\b(?:gemma|qwen|llama|claude|gpt|gemini|mistral|deepseek|chatgpt|copilot)[\w\-_.:]*/gi),t.push(/(?:我[是叫]|本人是|身為|身為一個?)\s*[\w\-_.:]{2,40}\s*(?:AI|助手|模型|語言模型|LLM|機器人)/gi),t.push(/(?:由|基於|透過|使用)\s*[\w\-_.:]{2,40}\s*(?:訓練|開發|構建|創建|研發|支持|驅動|賦能)/gi),t.push(/(?:作為|身為|身为|作為一個?)\s*(?:一個?\s*)?(?:AI|人工智慧|人工智能|LLM)(?:\s*(?:大型)?(?:語言|對話|生成)?模型)?/gi),t.push(/(?:AI|人工智慧|人工智能)\s*(?:大型)?(?:語言|對話)?模型/gi),t.push(/\bas an?\s+(?:AI|artificial intelligence|language model|LLM|assistant)\b/gi),t.push(/(?:我的|本|此)\s*(?:版本|模型|名字|名稱|身份)\s*(?:是|為|:)\s*[\w\-_.:]{2,40}/gi),{name:"self-intro",transform(e){let n=e;for(const e of t)n=n.replace(e,"");return n.replace(/[ \t]{2,}/g," ").replace(/\n[ \t]+/g,"\n").replace(/\n{3,}/g,"\n\n").trim()}}}function f(e,t,n){let o=e;for(const e of t)e.transform&&(o=e.transform(o,n));return o}const y=3,h=[d];function k(e,t,n){let o=e;const r=[];for(const e of t){if(!e.transform)continue;const t=e.transform(o,n);t!==o&&r.push(e.name),o=t}return{text:o,changed:r}}function w(n){const{domains:o}=n;async function*r(n){const r=o[n.domain];if(!r)throw new Error(`[chat-kit/engine] 未註冊的 domain:${n.domain}`);!function(e){const t=e[e.length-1];if(!t||"user"!==t.role)throw new Error("[chat-kit/engine] messages 的最後一則必須是 user");if(!t.content.trim())throw new Error("[chat-kit/engine] 使用者訊息不得為空")}(n.messages);let s=0;const a=n.messages.map(e=>{if("user"!==e.role)return e;const t=m(e.content);return s+=t.neutralized,{...e,content:t.text}});s>0&&(yield{type:"guard",name:"neutralize-markers",action:"neutralized",detail:`${s} 個標記`});const i=a[a.length-1].content,l={domain:n.domain,channel:n.channel,messages:a,userMessage:i,...void 0!==n.tenantId?{tenantId:n.tenantId}:{},...void 0!==n.entity?{entity:n.entity}:{},...void 0!==n.signal?{signal:n.signal}:{}};let c,u,d;try{c="function"==typeof r.prompt?await r.prompt(l):r.prompt;const e=await(r.buildContext?.(l))??[];u=p({spec:c,channel:n.channel,blocks:e,extraSystem:a.filter(e=>"system"===e.role).map(e=>e.content),userMessage:i}),d=await(r.tools?.(l))}catch(e){return void(yield{type:"error",error:e})}yield{type:"context",injectedKeys:u.injectedKeys};const g=a.filter(e=>"system"!==e.role).map((e,t,n)=>t===n.length-1?{role:"user",content:u.userMessage}:{role:e.role,content:e.content}),f=!!d&&Object.keys(d).length>0,y=(r.maxToolSteps??3)+1,w=n.modelOptions??{},x=e({model:n.model,system:u.system,messages:g,maxRetries:0,...f?{tools:d,stopWhen:t(y)}:{},...f&&r.prepareStep?{prepareStep:r.prepareStep}:{},...void 0!==w.temperature?{temperature:w.temperature}:{},...void 0!==w.maxOutputTokens?{maxOutputTokens:w.maxOutputTokens}:{},...void 0!==w.providerOptions?{providerOptions:w.providerOptions}:{},...void 0!==w.timeout?{timeout:w.timeout}:{},...n.signal?{abortSignal:n.signal}:{},onError:()=>{}}),O=[...h,...r.outputGuards??[]],T={modelId:(b=n.model,"string"==typeof b?b:b.modelId),channel:n.channel};var b;let E="",M=0,v="unknown",A=null,L=!1;for await(const e of x.fullStream){switch(e.type){case"text-delta":{E+=e.text;const{text:t}=k(E,O,T);t.length>M&&(yield{type:"content",text:t.slice(M)},M=t.length);break}case"reasoning-delta":e.text&&(yield{type:"thinking",text:e.text});break;case"tool-call":yield{type:"tool-call",toolName:e.toolName,input:e.input};break;case"tool-result":yield{type:"tool-result",toolName:e.toolName,output:e.output};break;case"error":L=!0,yield{type:"error",error:e.error};break;case"abort":L=!0,yield{type:"error",error:new Error("[chat-kit/engine] 串流已中止"+(e.reason?`:${e.reason}`:""))};break;case"finish":v=e.finishReason,A={inputTokens:e.totalUsage.inputTokens,outputTokens:e.totalUsage.outputTokens,reasoningTokens:e.totalUsage.outputTokenDetails?.reasoningTokens,totalTokens:e.totalUsage.totalTokens}}if(L)break}if(A&&(yield{type:"usage",usage:A}),L)return;const{text:R,changed:_}=k(E,O,T);for(const e of _)yield{type:"guard",name:e,action:"transformed"};let S=R,$=!1;for(const e of O){if(!e.check)continue;const t=await e.check(S,T);"rewrite"===t.action&&"string"==typeof t.text&&t.text!==S?(S=t.text,$=!0,yield{type:"guard",name:e.name,action:"rewrite",...t.reason?{detail:t.reason}:{}}):"pass"===t.action&&t.reason&&(yield{type:"guard",name:e.name,action:"observed",detail:t.reason})}yield{type:"done",text:S,rewritten:$,finishReason:v}}return{runTurn:r,completeTurn:async function(e){const t={text:"",thinking:"",rewritten:!1,finishReason:"unknown",usage:null,toolCalls:[],guards:[],injectedKeys:[]};for await(const n of r(e))switch(n.type){case"context":t.injectedKeys=n.injectedKeys;break;case"thinking":t.thinking+=n.text;break;case"tool-call":t.toolCalls.push({toolName:n.toolName,input:n.input});break;case"tool-result":{const e=[...t.toolCalls].reverse().find(e=>e.toolName===n.toolName&&!("output"in e));e&&(e.output=n.output);break}case"guard":t.guards.push({name:n.name,action:n.action,...n.detail?{detail:n.detail}:{}});break;case"usage":t.usage=n.usage;break;case"error":throw n.error;case"done":t.text=n.text,t.rewritten=n.rewritten,t.finishReason=n.finishReason}return t}}}export{y as DEFAULT_MAX_TOOL_STEPS,n as TRUST_LABELS,f as applyTransforms,p as assemblePrompt,r as closeMarker,w as createChatEngine,i as expandPlaceholders,s as markerPattern,m as neutralizeMarkers,o as openMarker,l as orderBlocks,c as renderBlock,g as selfIntroGuard,d as stripMarkersGuard};//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../.build/prompt/markers.js","../.build/prompt/assemble.js","../.build/guards/input.js","../.build/guards/output.js","../.build/engine/engine.js"],"names":["streamText","stepCountIs","CHAT_KIT_OPTIONS_KEY","DEFAULT_ANTHROPIC_MAX_OUTPUT_TOKENS","DEFAULT_COOLDOWN_MS","DEFAULT_PROTOCOL","MAX_COOLDOWN_MS","PROTOCOL_ALIASES","consoleLogger","createFallbackModel","createKeyRing","createLanguageModel","createModelFromConfig","createRoutedModel","defaultKeyRing","isOpenCodeEndpoint","isQuotaError","listModels","maskApiKey","normalizeApiKeyList","normalizeBaseURL","normalizeModelConfig","normalizeProtocol","openCodeHeaders","parseModelSpec","reasoningOptions","reasoningProviderOptions","resolveModelChain","resolveModelTarget","resolveProtocol","resolveProviderApiKeys","retryAfterMsFromError","TRUST_LABELS","authoritative","reference","realtime","openMarker","trust","title","closeMarker","markerPattern","labels","Object","values","join","RegExp","PLACEHOLDER","expandPlaceholders","text","leftover","Set","expanded","replace","match","name","prototype","hasOwnProperty","call","add","size","Error","orderBlocks","blocks","seen","b","has","key","filter","content","trim","length","slice","sort","a","order","renderBlock","block","format","defaultCompose","message","assemblePrompt","input","spec","channel","userMessage","ordered","systemBlocks","placement","userBlocks","system","parts","channelParts","map","t","placeholders","extraSystem","s","composeUserMessage","injectedKeys","neutralizeMarkers","neutralized","_m","inner","stripMarkersGuard","transform","buffer","selfIntroGuard","modelNames","patterns","push","cleaned","re","applyTransforms","guards","ctx","out","g","DEFAULT_MAX_TOOL_STEPS","BUILTIN_OUTPUT_GUARDS","transformWithTrace","changed","next","createChatEngine","options","domains","async","runTurn","domain","messages","last","role","validateMessages","m","r","type","action","detail","turn","undefined","tenantId","entity","signal","assembled","tools","prompt","buildContext","error","conversation","i","arr","hasTools","keys","maxSteps","maxToolSteps","opts","modelOptions","result","model","maxRetries","stopWhen","prepareStep","temperature","maxOutputTokens","providerOptions","timeout","abortSignal","onError","outputGuards","guardCtx","modelId","raw","sent","finishReason","usage","failed","part","fullStream","toolName","output","reason","inputTokens","totalUsage","outputTokens","reasoningTokens","outputTokenDetails","totalTokens","transformed","finalText","rewritten","check","verdict","completeTurn","thinking","toolCalls","ev","pending","reverse","find","c"],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 依協定建立單一金鑰的 AI SDK LanguageModel。
|
|
3
|
+
*
|
|
4
|
+
* provider 套件以動態 import 載入:三個都是 optional peer dependency,只裝自己用得到的
|
|
5
|
+
* 那個即可;缺套件時在**第一次呼叫**才報錯,並直接說要裝什麼。
|
|
6
|
+
*/
|
|
7
|
+
import type { LanguageModelV3 } from '@ai-sdk/provider';
|
|
8
|
+
import type { ModelTarget } from './types.js';
|
|
9
|
+
export type FetchFunction = typeof globalThis.fetch;
|
|
10
|
+
export interface CreateModelSettings {
|
|
11
|
+
headers?: Record<string, string>;
|
|
12
|
+
fetch?: FetchFunction;
|
|
13
|
+
}
|
|
14
|
+
/** 建立單一金鑰模型的函式型別;`createRoutedModel` 可注入替身做測試 */
|
|
15
|
+
export type ModelFactory = (target: ModelTarget, apiKey: string, settings: CreateModelSettings) => Promise<LanguageModelV3>;
|
|
16
|
+
/**
|
|
17
|
+
* 無金鑰端點(本機 Ollama)的 Bearer 佔位值,與舊實作相同。Ollama 不驗證它;
|
|
18
|
+
* 不送 Authorization 反而會讓部分 OpenAI 相容伺服器回 401。
|
|
19
|
+
*/
|
|
20
|
+
export declare const NO_KEY_BEARER_PLACEHOLDER = "ollama";
|
|
21
|
+
export declare const createLanguageModel: ModelFactory;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 主備援模型:依序嘗試多個 model,前一個失敗就換下一個。移植自 stock-viewer
|
|
3
|
+
* `server/utils/stream-fallback.js` 的換手規則。
|
|
4
|
+
*
|
|
5
|
+
* ⚠️ **一旦已產出文字內容就不再換手**:Web 的 chunk 已即時送到使用者眼前,此時換 provider
|
|
6
|
+
* 會讓兩個模型的回覆拼接在一起,比直接報錯更糟。只吐了 thinking、還沒有正文時仍可換手
|
|
7
|
+
* (與舊實作相同)。
|
|
8
|
+
*
|
|
9
|
+
* 每個候選自己負責轉譯推理強度等參數(見 `routed-model.ts`),所以同一組呼叫選項可以
|
|
10
|
+
* 原樣交給不同 provider。
|
|
11
|
+
*/
|
|
12
|
+
import type { LanguageModelV3 } from '@ai-sdk/provider';
|
|
13
|
+
import type { Logger } from './types.js';
|
|
14
|
+
export interface FallbackEvent {
|
|
15
|
+
from: LanguageModelV3;
|
|
16
|
+
to: LanguageModelV3;
|
|
17
|
+
error: unknown;
|
|
18
|
+
/** 換手後的候選索引 */
|
|
19
|
+
index: number;
|
|
20
|
+
}
|
|
21
|
+
export interface FallbackModelOptions {
|
|
22
|
+
onFallback?: (event: FallbackEvent) => void;
|
|
23
|
+
logger?: Logger;
|
|
24
|
+
}
|
|
25
|
+
export declare function createFallbackModel(models: readonly LanguageModelV3[], options?: FallbackModelOptions): LanguageModelV3;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 從 `providers` 設定(stock-viewer `config.json` 的形狀)組出 model。
|
|
3
|
+
*
|
|
4
|
+
* 移植自 stock-viewer `server/services/chat-config.js` 的「主 + 備援」解析,但不綁定設定區段的
|
|
5
|
+
* 位置(`premium.chat`、`ai-report` 都是呼叫端的事),也不內建預設 provider 或預設推理強度——
|
|
6
|
+
* 那些是各專案的產品決策。
|
|
7
|
+
*/
|
|
8
|
+
import type { LanguageModelV3 } from '@ai-sdk/provider';
|
|
9
|
+
import type { Logger, ModelTarget, ProviderMap, ReasoningEffort } from './types.js';
|
|
10
|
+
import type { ModelConfig, RawModelConfig } from './model-spec.js';
|
|
11
|
+
import type { RoutedModelOptions } from './routed-model.js';
|
|
12
|
+
import type { FallbackModelOptions } from './fallback-model.js';
|
|
13
|
+
export interface ResolveTargetOptions {
|
|
14
|
+
/** 指定模型;省略則用 provider 的 defaultModel */
|
|
15
|
+
modelName?: string | null;
|
|
16
|
+
/** 指定推理強度;省略則用 provider 的 reasoningEffort */
|
|
17
|
+
reasoningEffort?: ReasoningEffort;
|
|
18
|
+
/** 沒有金鑰就拋錯(雲端 provider 用);本機端點可關閉 */
|
|
19
|
+
requireApiKey?: boolean;
|
|
20
|
+
env?: Record<string, string | undefined>;
|
|
21
|
+
logger?: Logger;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 解析單一 provider 為呼叫目標。
|
|
25
|
+
* @throws provider 不存在、缺 baseURL、缺 model,或 `requireApiKey` 時缺金鑰
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveModelTarget(providers: ProviderMap, providerKey: string, options?: ResolveTargetOptions): ModelTarget;
|
|
28
|
+
export interface ResolveChainOptions extends Omit<ResolveTargetOptions, 'modelName' | 'reasoningEffort'> {
|
|
29
|
+
/** 設定未指定 provider 時的預設 */
|
|
30
|
+
defaultProviderKey?: string;
|
|
31
|
+
/** 設定與 provider 都沒給推理強度時的預設 */
|
|
32
|
+
defaultReasoningEffort?: ReasoningEffort;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 解析「主 + 備援」目標清單。
|
|
36
|
+
*
|
|
37
|
+
* - 備援**只沿用 reasoningEffort、不沿用 modelName**:modelName 屬於主 provider 的命名空間,
|
|
38
|
+
* 套到別的 provider 多半查無此模型,改用備援自己的 defaultModel。
|
|
39
|
+
* - 主設定無效直接拋出;備援無效只警告並略過——備援壞掉不該連累本來能用的主 provider。
|
|
40
|
+
*
|
|
41
|
+
* @returns 至少一筆(主),有有效備援時兩筆
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveModelChain(providers: ProviderMap, rawConfig: RawModelConfig | ModelConfig, options?: ResolveChainOptions): ModelTarget[];
|
|
44
|
+
export interface ModelFromConfigOptions extends ResolveChainOptions {
|
|
45
|
+
routed?: Omit<RoutedModelOptions, 'logger'>;
|
|
46
|
+
fallback?: Omit<FallbackModelOptions, 'logger'>;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 一步到位:設定 → 可直接交給 `streamText` 的 model(有備援時自動包成主備援)。
|
|
50
|
+
* 同時回傳解析出的目標清單,供呼叫端記錄用量或依換手結果調整行為。
|
|
51
|
+
*/
|
|
52
|
+
export declare function createModelFromConfig(providers: ProviderMap, rawConfig: RawModelConfig | ModelConfig, options?: ModelFromConfigOptions): {
|
|
53
|
+
model: LanguageModelV3;
|
|
54
|
+
targets: ModelTarget[];
|
|
55
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type { Logger, ModelTarget, Protocol, ProviderDef, ProviderMap, ReasoningEffort } from './types.js';
|
|
2
|
+
export { consoleLogger } from './types.js';
|
|
3
|
+
export { DEFAULT_PROTOCOL, PROTOCOL_ALIASES, normalizeProtocol, resolveProtocol } from './protocol.js';
|
|
4
|
+
export type { ModelConfig, ModelSpec, RawModelConfig } from './model-spec.js';
|
|
5
|
+
export { normalizeModelConfig, parseModelSpec } from './model-spec.js';
|
|
6
|
+
export type { KeyRing, KeyRingOptions, KeyRingStatus, KeyStrategy } from './key-ring.js';
|
|
7
|
+
export { DEFAULT_COOLDOWN_MS, MAX_COOLDOWN_MS, createKeyRing, defaultKeyRing, isQuotaError, maskApiKey, normalizeApiKeyList, resolveProviderApiKeys, retryAfterMsFromError, } from './key-ring.js';
|
|
8
|
+
export type { ClientIdentity } from './quirks.js';
|
|
9
|
+
export { DEFAULT_ANTHROPIC_MAX_OUTPUT_TOKENS, isOpenCodeEndpoint, normalizeBaseURL, openCodeHeaders, reasoningProviderOptions, } from './quirks.js';
|
|
10
|
+
export type { CreateModelSettings, FetchFunction, ModelFactory } from './create-model.js';
|
|
11
|
+
export { createLanguageModel } from './create-model.js';
|
|
12
|
+
export type { ChatKitCallOptions, RoutedModelOptions } from './routed-model.js';
|
|
13
|
+
export { CHAT_KIT_OPTIONS_KEY, createRoutedModel, reasoningOptions } from './routed-model.js';
|
|
14
|
+
export type { FallbackEvent, FallbackModelOptions } from './fallback-model.js';
|
|
15
|
+
export { createFallbackModel } from './fallback-model.js';
|
|
16
|
+
export type { ModelFromConfigOptions, ResolveChainOptions, ResolveTargetOptions } from './from-config.js';
|
|
17
|
+
export { createModelFromConfig, resolveModelChain, resolveModelTarget } from './from-config.js';
|
|
18
|
+
export type { ListedModel, ListModelsOptions } from './list-models.js';
|
|
19
|
+
export { listModels } from './list-models.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e={warn:e=>console.warn(e),info:e=>console.info(e)},t="openai-compatible",r=["openai-compatible","openai","anthropic"],o={chat:"openai-compatible",responses:"openai",messages:"anthropic"};function n(e){return"string"!=typeof e?null:r.includes(e)?e:Object.prototype.hasOwnProperty.call(o,e)?o[e]:null}function a(o,a,i=e){const s=o?.sdk;let c;if("string"==typeof s)c=s;else if(s&&"object"==typeof s){const e=a?s[a]:void 0;c=void 0!==e?e:s["*"]}if(null==c)return t;let l=c;"object"==typeof c&&(l=c.flavor,"string"==typeof c.path&&c.path&&i.warn(`[chat-kit/protocol] 不支援自訂端點路徑 "${c.path}"(model=${a||"?"}),已忽略,改用協定預設路徑`));const u=n(l);return u||(i.warn(`[chat-kit/protocol] 未知的協定 "${String(l)}"(model=${a||"?"}),退回 "${t}"。合法值:${r.join(" / ")}`),t)}function i(e){if("string"!=typeof e||!e)return{providerKey:null,modelName:null};const t=e.indexOf("/");return t<0?{providerKey:e,modelName:null}:{providerKey:e.substring(0,t)||null,modelName:e.substring(t+1)||null}}function s(e){return e?"string"==typeof e?{...i(e),reasoningEffort:void 0,backup:void 0}:e.primary?{providerKey:e.primary,modelName:e.model||null,reasoningEffort:e.reasoningEffort,backup:e.backup}:{...i(e.model),reasoningEffort:e.reasoningEffort,backup:e.backup}:{providerKey:null,modelName:null,reasoningEffort:void 0,backup:void 0}}const c=9e5,l=864e5;function u(t={}){const r=t.strategy??"round-robin",o=Number.isFinite(t.defaultCooldownMs)&&t.defaultCooldownMs>0?t.defaultCooldownMs:c,n=t.logger??e,a=t.now??Date.now,i=t.random??Math.random,s=new Map;function u(e){const t=function(e){return(e||"").replace(/\/+$/,"").toLowerCase()}(e);let r=s.get(t);return r||(r={cursor:0,cooldowns:new Map,knownKeys:new Set},s.set(t,r)),r}return{pick(e,t){const o=t.filter(e=>"string"==typeof e&&e.length>0);if(0===o.length)return"";const s=u(e);for(const e of o)s.knownKeys.add(e);if(1===o.length)return o[0];const c=a(),l=o.filter(e=>!((s.cooldowns.get(e)??0)>c));if(0===l.length)return n.warn(`[chat-kit/key-ring] ${e} 的 ${o.length} 把金鑰全部處於冷卻中,改用最快解除的一把`),o.reduce((e,t)=>(s.cooldowns.get(t)??0)<(s.cooldowns.get(e)??0)?t:e);if("random"===r)return l[Math.floor(i()*l.length)];const f=l[s.cursor%l.length];return s.cursor=(s.cursor+1)%Number.MAX_SAFE_INTEGER,f},markExhausted(e,t,r=null){if(!t)return;const i="number"==typeof r&&Number.isFinite(r)&&r>0?Math.min(r,l):o,s=u(e);s.knownKeys.add(t),s.cooldowns.set(t,a()+i),n.warn(`[chat-kit/key-ring] ${e} 金鑰 ${g(t)} 配額耗盡,冷卻 ${Math.round(i/1e3)}s`)},status(){const e=a(),t=[];for(const[r,o]of s){const n=o.knownKeys.size,a=[...o.cooldowns.values()].filter(t=>t>e),i=n>1?a.length:0;t.push({baseURL:r,total:n,coolingDown:i,nextRecoveryMs:i>0?Math.min(...a)-e:null})}return t},reset(){s.clear()}}}const f=u();function d(e,t){return e.replace(/\$\{([^}]+)\}/g,(e,r)=>t[r]||"")}function p(e,t=process.env){const r=Array.isArray(e)?e:[e],o=[];for(const e of r){if("string"!=typeof e)continue;const r=d(e,t).trim();r&&!o.includes(r)&&o.push(r)}return o}function h(e,t=process.env){if(!e||"object"!=typeof e)return[];const r=e=>null==e?[]:Array.isArray(e)?e:[e];return p([...r(e.apiKey),...r(e.apiKeys)],t)}function g(e){return"string"!=typeof e||0===e.length?"(none)":e.length<=4?"****":`****${e.slice(-4)}`}function m(e){const t=[];let r=e;for(let e=0;r&&e<5;e++){t.push(r);const e=r;r=e.lastError??e.cause}return t}function y(e){for(const t of m(e)){const e=t,r=e.statusCode??e.status??e.response?.status;if(429===r||"429"===r)return!0;const o=`${e.message??""} ${e.error?.message??""}`.toLowerCase();if(o.trim()&&/\b429\b|rate.?limit|too many requests|quota|insufficient_quota|resource[_ ]exhausted/.test(o))return!0}return!1}function w(e,t=Date.now){for(const r of m(e)){const e=r,o=e.responseHeaders??e.headers;if(!o)continue;const n=e=>{const t=o;if("function"==typeof t.get)return t.get(e);const r=Object.keys(t).find(t=>t.toLowerCase()===e);return r?t[r]:void 0},a=parseFloat(n("retry-after-ms")??"");if(Number.isFinite(a)&&a>0)return a;const i=n("retry-after");if(null==i)continue;const s=parseFloat(i);if(Number.isFinite(s)&&s>0)return 1e3*s;const c=Date.parse(i);if(Number.isFinite(c)){const e=c-t();return e>0?e:null}}return null}function k(e){if(!e)return e;let t=e.replace(/\/+$/,"");return t.endsWith("/v1")||(t+="/v1"),t}const b="opencode.ai";function v(e){const t=function(e){try{return new URL(e).hostname.toLowerCase()}catch{return null}}(e);return t===b||(t?.endsWith(`.${b}`)??!1)}function E(e,t){return t&&v(e)?{"User-Agent":t.userAgent,"x-opencode-session":t.sessionId}:{}}function $(e,t,r){if(!r)return{};switch(e){case"openai-compatible":return(e=>e.toLowerCase().includes("groq.com"))(t)?{}:(e=>e.toLowerCase().includes("agnes-ai"))(t)?{chat_template_kwargs:{enable_thinking:"high"===r}}:{reasoningEffort:r};case"openai":return{reasoningEffort:r,forceReasoning:!0,systemMessageMode:"system"};case"anthropic":return{}}}const K=8192;function M(e){return e.replace(/[^A-Za-z0-9_-]/g,"-")||"provider"}async function R(e,t){try{return await t()}catch(t){const r=t.code;if("ERR_MODULE_NOT_FOUND"===r||"MODULE_NOT_FOUND"===r)throw new Error(`[chat-kit/models] 此協定需要安裝 ${e}:npm install ${e}`);throw t}}const A=async(e,t,r)=>{const o=k(e.baseURL),{headers:n,fetch:a}=r,i=t||("anthropic"===e.protocol?"":"ollama");switch(e.protocol){case"openai-compatible":{const{createOpenAICompatible:t}=await R("@ai-sdk/openai-compatible",()=>import("@ai-sdk/openai-compatible"));return t({name:M(e.providerKey),baseURL:o,apiKey:i,...n?{headers:n}:{},...a?{fetch:a}:{},includeUsage:!0}).chatModel(e.model)}case"openai":{const{createOpenAI:t}=await R("@ai-sdk/openai",()=>import("@ai-sdk/openai"));return t({baseURL:o,apiKey:i,...n?{headers:n}:{},...a?{fetch:a}:{}}).responses(e.model)}case"anthropic":{const{createAnthropic:t}=await R("@ai-sdk/anthropic",()=>import("@ai-sdk/anthropic"));return t({baseURL:o,apiKey:i,...n?{headers:n}:{},...a?{fetch:a}:{}}).languageModel(e.model)}}};async function L(e,t){const r=e.getReader(),o=[];let n=!1;for(;;){const e=await r.read();if(e.done){n=!0;break}const a=e.value;if("error"===a.type&&!o.some(t))return r.cancel().catch(()=>{}),{kind:"early-error",error:a.error};if(o.push(a),t(a)||"error"===a.type)break}return{kind:"ok",stream:new ReadableStream({start(e){for(const t of o)e.enqueue(t);n&&e.close()},async pull(e){const t=await r.read();t.done?e.close():e.enqueue(t.value)},cancel:e=>r.cancel(e)})}}const U=e=>"text-delta"===e.type,N=e=>"text-delta"===e.type||"reasoning-delta"===e.type,O="chatKit";function x(e){return{[O]:{reasoningEffort:e}}}function _(e,t){const{[O]:r,...o}=t.providerOptions??{},n=r??{},a="reasoningEffort"in n?n.reasoningEffort:e.reasoningEffort,i=$(e.protocol,e.baseURL,a),s="openai-compatible"===e.protocol?M(e.providerKey):"openai"===e.protocol?"openai":"anthropic",c={...o};Object.keys(i).length>0&&(c[s]={...c[s]??{},...i});const l={...t,providerOptions:c};return"anthropic"===e.protocol&&null==l.maxOutputTokens&&(l.maxOutputTokens=8192),l}function C(t,r={}){const o=r.keyRing??f,n=r.logger??e,a=r.createModel??A,i=E(t.baseURL,r.identity),s={...Object.keys(i).length>0?{headers:i}:{},...r.fetch?{fetch:r.fetch}:{}},c=new Map;function l(e){let r=c.get(e);return r||(r=a(t,e,s),r.catch(()=>c.delete(e)),c.set(e,r)),r}async function u(e){const r=t.apiKeys,a=new Set;let i=null;for(let s=0;s<Math.max(1,r.length);s++){const s=r.filter(e=>!a.has(e));if(r.length>0&&0===s.length)break;const c=o.pick(t.baseURL,s);a.add(c);try{return await e(await l(c))}catch(e){if(i=e,!y(e))throw e;if(o.markExhausted(t.baseURL,c,w(e)),!r.some(e=>!a.has(e)))throw e;n.warn(`[chat-kit/routed] ${t.providerKey} 換用下一把金鑰重試(已試 ${a.size}/${r.length})`)}}throw i??new Error(`[chat-kit/routed] ${t.providerKey} 沒有可用的 API key`)}return{specificationVersion:"v3",provider:`chat-kit.${t.providerKey}`,modelId:t.model,supportedUrls:{},doGenerate(e){const r=_(t,e);return u(e=>Promise.resolve(e.doGenerate(r)))},doStream(e){const r=_(t,e);return u(async e=>{const t=await e.doStream(r),o=await L(t.stream,N);if("early-error"===o.kind)throw o.error;return{...t,stream:o.stream}})}}}function F(e){return`${e.provider}/${e.modelId}`}function I(e){return e instanceof Error?e.message:String(e)}function S(t,r={}){if(0===t.length)throw new Error("[chat-kit/fallback] 至少需要一個候選 model");if(1===t.length)return t[0];const o=r.logger??e;async function n(e){let n=null;for(let a=0;a<t.length;a++){const i=t[a];try{return await e(i)}catch(e){n=e;const s=t[a+1];if(!s)break;o.warn(`[chat-kit/fallback] ${F(i)} 失敗(${I(e)}),換手至 ${F(s)}`),r.onFallback?.({from:i,to:s,error:e,index:a+1})}}throw n}const a=t[0];return{specificationVersion:"v3",provider:"chat-kit.fallback",modelId:a.modelId,supportedUrls:a.supportedUrls,doGenerate:e=>n(t=>Promise.resolve(t.doGenerate(e))),doStream:e=>n(async t=>{const r=await t.doStream(e),o=await L(r.stream,U);if("early-error"===o.kind)throw o.error;return{...r,stream:o.stream}})}}function j(e,t,r={}){const o=e[t];if(!o)throw new Error(`[chat-kit/config] provider "${t}" 未設定`);const n=(o.baseURL||"").replace(/\/+$/,"");if(!n)throw new Error(`[chat-kit/config] provider "${t}" 缺少 baseURL`);const i=r.modelName||o.defaultModel;if(!i)throw new Error(`[chat-kit/config] provider "${t}" 沒有可用的模型(未指定且無 defaultModel)`);const s=h(o,r.env);if(r.requireApiKey&&0===s.length)throw new Error(`[chat-kit/config] provider "${t}" 未設定 API key`);const c=r.reasoningEffort??o.reasoningEffort;return{providerKey:t,baseURL:n,apiKeys:s,model:i,protocol:a(o,i,r.logger),...c?{reasoningEffort:c}:{}}}function q(t,r,o={}){const n=o.logger??e,a=function(e){return!!e&&"object"==typeof e&&"providerKey"in e&&"modelName"in e}(r)?r:s(r),i=a.providerKey||o.defaultProviderKey;if(!i)throw new Error("[chat-kit/config] 未指定 provider,也沒有 defaultProviderKey");const c=e=>a.reasoningEffort??t[e]?.reasoningEffort??o.defaultReasoningEffort,l={...void 0!==o.requireApiKey?{requireApiKey:o.requireApiKey}:{},env:o.env,logger:n},u=c(i),f=[j(t,i,{...l,modelName:a.modelName,...u?{reasoningEffort:u}:{}})],d=a.backup;if(d&&d!==i)try{const e=c(d);f.push(j(t,d,{...l,...e?{reasoningEffort:e}:{}}))}catch(e){n.warn(`[chat-kit/config] 備援 provider "${d}" 無法使用,將僅使用主 provider:${e.message}`)}return f}function P(t,r,o={}){const n=o.logger??e,a=q(t,r,o);return{model:S(a.map(e=>C(e,{...o.routed,logger:n})),{...o.fallback,logger:n}),targets:a}}class T extends Error{status;responseHeaders;constructor(e,t,r){super(`listModels HTTP ${e}${t?`: ${t}`:""}`),this.name="ListModelsHttpError",this.status=e,this.responseHeaders={},r.forEach((e,t)=>{this.responseHeaders[t]=e})}}async function D(t,r,o={}){const n=o.keyRing??f,a=o.logger??e,i=o.fetch??globalThis.fetch,s=p(r),c=`${k(t)}/models`,l=E(t,o.identity),u=new Set;let d=null;for(let e=0;e<Math.max(1,s.length);e++){const e=s.filter(e=>!u.has(e));if(s.length>0&&0===e.length)break;const r=n.pick(t,e);u.add(r);try{const e=await i(c,{method:"GET",headers:{...r?{Authorization:`Bearer ${r}`}:{},...l},...o.signal?{signal:o.signal}:{}});if(!e.ok){const t=await e.text().catch(()=>"");throw new T(e.status,t.slice(0,200),e.headers)}const t=await e.json();if(!Array.isArray(t?.data))throw new Error("[chat-kit/models] listModels 回應格式不符:缺少 data 陣列");return t.data.filter(e=>"string"==typeof e?.id).map(e=>({id:e.id,name:e.id}))}catch(e){if(d=e,!y(e))throw e;if(n.markExhausted(t,r,w(e)),!s.some(e=>!u.has(e)))throw e;a.warn(`[chat-kit/list-models] ${t} 換用下一把金鑰重試(已試 ${u.size}/${s.length})`)}}throw d??new Error("[chat-kit/models] listModels:沒有可用的 API key")}export{O as CHAT_KIT_OPTIONS_KEY,K as DEFAULT_ANTHROPIC_MAX_OUTPUT_TOKENS,c as DEFAULT_COOLDOWN_MS,t as DEFAULT_PROTOCOL,l as MAX_COOLDOWN_MS,o as PROTOCOL_ALIASES,e as consoleLogger,S as createFallbackModel,u as createKeyRing,A as createLanguageModel,P as createModelFromConfig,C as createRoutedModel,f as defaultKeyRing,v as isOpenCodeEndpoint,y as isQuotaError,D as listModels,g as maskApiKey,p as normalizeApiKeyList,k as normalizeBaseURL,s as normalizeModelConfig,n as normalizeProtocol,E as openCodeHeaders,i as parseModelSpec,x as reasoningOptions,$ as reasoningProviderOptions,q as resolveModelChain,j as resolveModelTarget,a as resolveProtocol,h as resolveProviderApiKeys,w as retryAfterMsFromError};//# sourceMappingURL=index.js.map
|