@thebtf/dsh-cliproxyapi-provider 0.4.3 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cordis.patch.yml CHANGED
@@ -1,5 +1,5 @@
1
- # Install the CLIProxyAPI catalog discovery and browser settings integration.
2
- # The existing pi-ai route remains in use until an explicit CPA cutover.
3
- - insert:
4
- - id: llm-cliproxyapi
5
- name: "@thebtf/dsh-cliproxyapi-provider"
1
+ # Install the CLIProxyAPI catalog discovery and browser settings integration.
2
+ # The existing pi-ai route remains in use until an explicit CPA cutover.
3
+ - insert:
4
+ - id: llm-cliproxyapi
5
+ name: "@thebtf/dsh-cliproxyapi-provider"
package/details/en.md CHANGED
@@ -1,9 +1,9 @@
1
- # CLIProxyAPI for DeepSeek Harness
2
-
3
- Open **Plugins**, then select **CLIProxyAPI**. Choose the wire protocol your gateway exposes — OpenAI Responses, OpenAI Chat Completions, Anthropic Messages or Google Generative AI — then enter your actual endpoint and explicitly choose keep, replace, reference, or no authentication. HTTP is unencrypted. A changed endpoint cannot reuse saved credentials without consent.
4
-
5
- Preview models, search or select all/deselect all, then save the connection, policy and model draft together. Zero selected models is valid but not ready. Details show metadata, source labels and advertised reasoning efforts. A stale catalog can remain available after a refresh failure. Save and activation are reported separately.
6
-
7
- Disable retains the configuration and credential; forget-key is separate. Saving the first connection adopts an existing legacy profile automatically, with a reversible backup; **Import legacy connection** is still available for an explicit import. The plugin also keeps a copy of the last saved connection outside the profile: when a rebuild or a core gate removes the settings entry, **Restore saved connection** brings it back without retyping the address or key. Inspect recovery errors before retrying. Do not overwrite newer settings with an old rollback backup.
8
-
9
- The Models footer opens Plugins; select CLIProxyAPI there. It does not provide a full managed Models row. Restart DSH after installing or updating. Requires Node 24.9.0 or newer and DSH 0.2.2-rc.3; `engines.dsh` accepts the rest of the 0.2.x line.
1
+ # CLIProxyAPI for DeepSeek Harness
2
+
3
+ Open **Plugins**, then select **CLIProxyAPI**. Choose the wire protocol your gateway exposes — OpenAI Responses, OpenAI Chat Completions, Anthropic Messages or Google Generative AI — then enter your actual endpoint and explicitly choose keep, replace, reference, or no authentication. HTTP is unencrypted. A changed endpoint cannot reuse saved credentials without consent.
4
+
5
+ Preview models, search or select all/deselect all, then save the connection, policy and model draft together. Zero selected models is valid but not ready. Details show metadata, source labels and advertised reasoning efforts. A stale catalog can remain available after a refresh failure. Save and activation are reported separately.
6
+
7
+ Disable retains the configuration and credential; forget-key is separate. Saving the first connection adopts an existing legacy profile automatically, with a reversible backup; **Import legacy connection** is still available for an explicit import. The plugin also keeps a copy of the last saved connection outside the profile: when a rebuild or a core gate removes the settings entry, **Restore saved connection** brings it back without retyping the address or key. Inspect recovery errors before retrying. Do not overwrite newer settings with an old rollback backup.
8
+
9
+ The Models footer opens Plugins; select CLIProxyAPI there. It does not provide a full managed Models row. Restart DSH after installing or updating. Requires Node 24.9.0 or newer and DSH 0.2.2-rc.3; `engines.dsh` accepts the rest of the 0.2.x line.
package/details/ru.md CHANGED
@@ -1,9 +1,9 @@
1
- # CLIProxyAPI для DeepSeek Harness
2
-
3
- Откройте **Plugins** и выберите **CLIProxyAPI**. Выберите протокол обмена, который предоставляет шлюз (OpenAI Responses, OpenAI Chat Completions, Anthropic Messages или Google Generative AI), затем укажите фактический адрес и явно выберите действие с авторизацией: сохранить, заменить ключ, использовать ссылку или отключить авторизацию. HTTP не шифруется. Повторное использование ключа с новым адресом требует согласия.
4
-
5
- Загрузите предпросмотр, найдите и выберите модели; массовые действия применяются ко всему каталогу. Сохранение включает адрес, авторизацию, политику и выбор. Ноль моделей допустим, но подключение не готово. Детали содержат метаданные, источник возможностей и заявленные уровни рассуждения. При ошибке обновления прежний каталог может быть показан как устаревший. Сохранение и активация показываются отдельно.
6
-
7
- Отключение сохраняет настройки и ключ; удаление ключа выполняется отдельно. При сохранении первого подключения прежняя запись принимается автоматически с обратимой резервной копией; **Импорт прежнего подключения** по-прежнему доступен для явного импорта. Плагин также хранит копию последнего сохранённого подключения вне профиля: если запись настроек исчезла после пересборки профиля или блокировки по совместимости, кнопка **Восстановить сохранённое подключение** вернёт её без повторного ввода адреса и ключа. Изучите ошибки восстановления перед повтором. Не восстанавливайте старую копию поверх новых настроек.
8
-
9
- Внизу Models можно открыть Plugins и затем выбрать CLIProxyAPI. Это не полноценная управляемая строка Models. После установки или обновления перезапустите DSH. Требуются Node от 24.9.0 и DSH 0.2.2-rc.3; `engines.dsh` допускает остальную линию 0.2.x.
1
+ # CLIProxyAPI для DeepSeek Harness
2
+
3
+ Откройте **Plugins** и выберите **CLIProxyAPI**. Выберите протокол обмена, который предоставляет шлюз (OpenAI Responses, OpenAI Chat Completions, Anthropic Messages или Google Generative AI), затем укажите фактический адрес и явно выберите действие с авторизацией: сохранить, заменить ключ, использовать ссылку или отключить авторизацию. HTTP не шифруется. Повторное использование ключа с новым адресом требует согласия.
4
+
5
+ Загрузите предпросмотр, найдите и выберите модели; массовые действия применяются ко всему каталогу. Сохранение включает адрес, авторизацию, политику и выбор. Ноль моделей допустим, но подключение не готово. Детали содержат метаданные, источник возможностей и заявленные уровни рассуждения. При ошибке обновления прежний каталог может быть показан как устаревший. Сохранение и активация показываются отдельно.
6
+
7
+ Отключение сохраняет настройки и ключ; удаление ключа выполняется отдельно. При сохранении первого подключения прежняя запись принимается автоматически с обратимой резервной копией; **Импорт прежнего подключения** по-прежнему доступен для явного импорта. Плагин также хранит копию последнего сохранённого подключения вне профиля: если запись настроек исчезла после пересборки профиля или блокировки по совместимости, кнопка **Восстановить сохранённое подключение** вернёт её без повторного ввода адреса и ключа. Изучите ошибки восстановления перед повтором. Не восстанавливайте старую копию поверх новых настроек.
8
+
9
+ Внизу Models можно открыть Plugins и затем выбрать CLIProxyAPI. Это не полноценная управляемая строка Models. После установки или обновления перезапустите DSH. Требуются Node от 24.9.0 и DSH 0.2.2-rc.3; `engines.dsh` допускает остальную линию 0.2.x.
package/details/zh.md CHANGED
@@ -1,9 +1,9 @@
1
- # DeepSeek Harness 的 CLIProxyAPI
2
-
3
- 打开 **插件**,然后选择 **CLIProxyAPI**。先选择网关提供的通信协议(OpenAI Responses、OpenAI Chat Completions、Anthropic Messages 或 Google Generative AI),再输入实际地址,并明确选择保留、替换密钥、凭据引用或不验证。HTTP 不加密。更换地址后,重用保存的凭据需要明确同意。
4
-
5
- 预览目录后搜索、选择或批量选择模型。保存同时提交连接、策略和模型草稿。允许零模型,但连接尚未就绪。详情展示元数据、来源和已声明的推理等级;刷新失败后,上次目录可能标为过期。保存和激活分别显示。
6
-
7
- 停用会保留设置和凭据;忘记密钥是独立操作。保存第一个连接时会自动接管旧配置并保留可回退的备份;仍可显式选择导入旧连接。插件还在配置文件之外保留最后保存连接的副本:如果配置文件重建或核心兼容性门禁移除了设置条目,页面上的 **恢复已保存的连接** 可直接恢复,无需重新输入地址和密钥。重试前检查恢复错误。不要用旧备份覆盖较新设置。
8
-
9
- Models 页底部可打开插件,然后选择 CLIProxyAPI;这不是完整的 Models 管理行。安装或更新后重启 DSH。需要 Node 24.9.0 及以上和 DSH 0.2.2-rc.3;`engines.dsh` 接受 0.2.x 的其余版本。
1
+ # DeepSeek Harness 的 CLIProxyAPI
2
+
3
+ 打开 **插件**,然后选择 **CLIProxyAPI**。先选择网关提供的通信协议(OpenAI Responses、OpenAI Chat Completions、Anthropic Messages 或 Google Generative AI),再输入实际地址,并明确选择保留、替换密钥、凭据引用或不验证。HTTP 不加密。更换地址后,重用保存的凭据需要明确同意。
4
+
5
+ 预览目录后搜索、选择或批量选择模型。保存同时提交连接、策略和模型草稿。允许零模型,但连接尚未就绪。详情展示元数据、来源和已声明的推理等级;刷新失败后,上次目录可能标为过期。保存和激活分别显示。
6
+
7
+ 停用会保留设置和凭据;忘记密钥是独立操作。保存第一个连接时会自动接管旧配置并保留可回退的备份;仍可显式选择导入旧连接。插件还在配置文件之外保留最后保存连接的副本:如果配置文件重建或核心兼容性门禁移除了设置条目,页面上的 **恢复已保存的连接** 可直接恢复,无需重新输入地址和密钥。重试前检查恢复错误。不要用旧备份覆盖较新设置。
8
+
9
+ Models 页底部可打开插件,然后选择 CLIProxyAPI;这不是完整的 Models 管理行。安装或更新后重启 DSH。需要 Node 24.9.0 及以上和 DSH 0.2.2-rc.3;`engines.dsh` 接受 0.2.x 的其余版本。
package/locale/en.json CHANGED
@@ -1,7 +1,7 @@
1
- {
2
- "meta": {
3
- "title": "CLIProxyAPI",
4
- "description": "Connect a CLIProxyAPI gateway and manage its models in DeepSeek Harness.",
5
- "detail": "details/en.md"
6
- }
7
- }
1
+ {
2
+ "meta": {
3
+ "title": "CLIProxyAPI",
4
+ "description": "Connect a CLIProxyAPI gateway and manage its models in DeepSeek Harness.",
5
+ "detail": "details/en.md"
6
+ }
7
+ }
package/locale/ru.json CHANGED
@@ -1,7 +1,7 @@
1
- {
2
- "meta": {
3
- "title": "CLIProxyAPI",
4
- "description": "Подключение шлюза CLIProxyAPI и управление моделями в DeepSeek Harness.",
5
- "detail": "details/ru.md"
6
- }
7
- }
1
+ {
2
+ "meta": {
3
+ "title": "CLIProxyAPI",
4
+ "description": "Подключение шлюза CLIProxyAPI и управление моделями в DeepSeek Harness.",
5
+ "detail": "details/ru.md"
6
+ }
7
+ }
package/locale/zh.json CHANGED
@@ -1,7 +1,7 @@
1
- {
2
- "meta": {
3
- "title": "CLIProxyAPI",
4
- "description": "在 DeepSeek Harness 中连接 CLIProxyAPI 网关并管理模型。",
5
- "detail": "details/zh.md"
6
- }
7
- }
1
+ {
2
+ "meta": {
3
+ "title": "CLIProxyAPI",
4
+ "description": "在 DeepSeek Harness 中连接 CLIProxyAPI 网关并管理模型。",
5
+ "detail": "details/zh.md"
6
+ }
7
+ }
package/package.json CHANGED
@@ -1,85 +1,85 @@
1
- {
2
- "name": "@thebtf/dsh-cliproxyapi-provider",
3
- "version": "0.4.3",
4
- "description": "GUI-configurable CLIProxyAPI provider for DeepSeek Harness with a selectable wire protocol",
5
- "type": "module",
6
- "main": "./src/index.js",
7
- "exports": {
8
- ".": "./src/index.js",
9
- "./client": "./client.js",
10
- "./catalog": "./src/catalog.js",
11
- "./cordis.patch.yml": "./cordis.patch.yml",
12
- "./locale/*.json": "./locale/*.json",
13
- "./package.json": "./package.json"
14
- },
15
- "files": [
16
- "src",
17
- "client.js",
18
- "locale",
19
- "details",
20
- "cordis.patch.yml",
21
- "README.md",
22
- "README_EN.md"
23
- ],
24
- "scripts": {
25
- "test": "node --test",
26
- "check": "node --check src/index.js && node --check src/catalog.js && node --check client.js && node --test",
27
- "pack:github": "npm pack"
28
- },
29
- "dsh": {
30
- "bundle": {
31
- "patch": "./cordis.patch.yml"
32
- },
33
- "client": {
34
- "inject": [
35
- "@deepseek-ai/dsh-client-locale",
36
- "@deepseek-ai/dsh-client-ui-settings",
37
- "@deepseek-ai/dsh-client-ui-primitives",
38
- "@deepseek-ai/dsh-client-ui-plugin-manager",
39
- "@deepseek-ai/dsh-api-remotes"
40
- ],
41
- "platform": "web"
42
- }
43
- },
44
- "repository": {
45
- "type": "git",
46
- "url": "git+https://github.com/thebtf/dsh-cliproxyapi-provider.git"
47
- },
48
- "peerDependencies": {
49
- "@deepseek-ai/cordis": "^4.0.4",
50
- "@deepseek-ai/dsh-credentials": "^0.2.2-rc.2",
51
- "@deepseek-ai/dsh-llm": "^0.2.2-rc.2",
52
- "@deepseek-ai/dsh-settings": "^0.2.2-rc.2"
53
- },
54
- "dependencies": {
55
- "@deepseek-ai/dsh-home-paths": "^0.2.2-rc.2",
56
- "@deepseek-ai/dsh-llm-pi-ai": "^0.2.2-rc.2",
57
- "@deepseek-ai/dsh-util-values": "^0.2.2-rc.2",
58
- "@earendil-works/pi-ai": "^1.3.0",
59
- "@deepseek-ai/schemastery": "^3.18.2"
60
- },
61
- "devDependencies": {
62
- "@deepseek-ai/dsh-api-remotes": "^0.2.2-rc.2",
63
- "@deepseek-ai/dsh-client-locale": "^0.2.2-rc.2",
64
- "@deepseek-ai/dsh-client-ui-settings": "^0.2.2-rc.2",
65
- "@deepseek-ai/dsh-client-ui-primitives": "^0.2.2-rc.2",
66
- "@deepseek-ai/dsh-client-ui-plugin-manager": "^0.2.2-rc.2",
67
- "@deepseek-ai/dsh-client-ui-slots": "^0.2.2-rc.2",
68
- "react": "^18.2.0"
69
- },
70
- "engines": {
71
- "node": ">=24.9.0",
72
- "dsh": "^0.2.2-rc.2"
73
- },
74
- "keywords": [
75
- "dsh",
76
- "dsh-plugin",
77
- "deepseek-harness",
78
- "cliproxyapi",
79
- "llm"
80
- ],
81
- "publishConfig": {
82
- "access": "public"
83
- },
84
- "license": "MIT"
85
- }
1
+ {
2
+ "name": "@thebtf/dsh-cliproxyapi-provider",
3
+ "version": "0.4.4",
4
+ "description": "GUI-configurable CLIProxyAPI provider for DeepSeek Harness with a selectable wire protocol",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./client": "./client.js",
10
+ "./catalog": "./src/catalog.js",
11
+ "./cordis.patch.yml": "./cordis.patch.yml",
12
+ "./locale/*.json": "./locale/*.json",
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "src",
17
+ "client.js",
18
+ "locale",
19
+ "details",
20
+ "cordis.patch.yml",
21
+ "README.md",
22
+ "README_EN.md"
23
+ ],
24
+ "scripts": {
25
+ "test": "node --test",
26
+ "check": "node --check src/index.js && node --check src/catalog.js && node --check client.js && node --test",
27
+ "pack:github": "npm pack"
28
+ },
29
+ "dsh": {
30
+ "bundle": {
31
+ "patch": "./cordis.patch.yml"
32
+ },
33
+ "client": {
34
+ "inject": [
35
+ "@deepseek-ai/dsh-client-locale",
36
+ "@deepseek-ai/dsh-client-ui-settings",
37
+ "@deepseek-ai/dsh-client-ui-primitives",
38
+ "@deepseek-ai/dsh-client-ui-plugin-manager",
39
+ "@deepseek-ai/dsh-api-remotes"
40
+ ],
41
+ "platform": "web"
42
+ }
43
+ },
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/thebtf/dsh-cliproxyapi-provider.git"
47
+ },
48
+ "peerDependencies": {
49
+ "@deepseek-ai/cordis": "^4.0.4",
50
+ "@deepseek-ai/dsh-credentials": "^0.2.2-rc.2",
51
+ "@deepseek-ai/dsh-llm": "^0.2.2-rc.2",
52
+ "@deepseek-ai/dsh-settings": "^0.2.2-rc.2"
53
+ },
54
+ "dependencies": {
55
+ "@deepseek-ai/dsh-home-paths": "^0.2.2-rc.2",
56
+ "@deepseek-ai/dsh-llm-pi-ai": "^0.2.2-rc.2",
57
+ "@deepseek-ai/dsh-util-values": "^0.2.2-rc.2",
58
+ "@earendil-works/pi-ai": "^1.3.0",
59
+ "@deepseek-ai/schemastery": "^3.18.2"
60
+ },
61
+ "devDependencies": {
62
+ "@deepseek-ai/dsh-api-remotes": "^0.2.2-rc.2",
63
+ "@deepseek-ai/dsh-client-locale": "^0.2.2-rc.2",
64
+ "@deepseek-ai/dsh-client-ui-settings": "^0.2.2-rc.2",
65
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.2.2-rc.2",
66
+ "@deepseek-ai/dsh-client-ui-plugin-manager": "^0.2.2-rc.2",
67
+ "@deepseek-ai/dsh-client-ui-slots": "^0.2.2-rc.2",
68
+ "react": "^18.2.0"
69
+ },
70
+ "engines": {
71
+ "node": ">=24.9.0",
72
+ "dsh": "^0.2.2-rc.2"
73
+ },
74
+ "keywords": [
75
+ "dsh",
76
+ "dsh-plugin",
77
+ "deepseek-harness",
78
+ "cliproxyapi",
79
+ "llm"
80
+ ],
81
+ "publishConfig": {
82
+ "access": "public"
83
+ },
84
+ "license": "MIT"
85
+ }
package/src/adapter.js CHANGED
@@ -1,68 +1,68 @@
1
- import { PiAiAdapter } from '@deepseek-ai/dsh-llm-pi-ai'
2
- import { LlmError, resolveImageAttachmentAccess, resolveRetryPolicy } from '@deepseek-ai/dsh-llm'
3
- import { createProvider, InMemoryCredentialStore, defaultProviderAuthContext } from '@earendil-works/pi-ai'
4
- import { createTransport } from './transport.js'
5
- import { resolveIdentity } from './connection.js'
6
- import { protocolOf } from './policy.js'
7
-
8
- const PROVIDER = 'CLIProxyAPI'
9
- const LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']
10
-
11
- export function createGatewayAdapter(ctx, snapshot, diagnostics, onMissing = () => { }) {
12
- let profiles = new Map()
13
- const update = (next) => {
14
- const captured = structuredClone(next)
15
- const protocol = protocolOf(captured.policy)
16
- const models = captured.models.filter((model) => captured.modelSelection?.[model.id] !== false).map((model) => {
17
- const efforts = model.reasoningEfforts
18
- const thinkingLevelMap = efforts && Object.fromEntries(LEVELS.flatMap((level) => efforts[level] === undefined ? [[level, null]] : efforts[level] === null ? [] : [[level, efforts[level]]]))
19
- return {
20
- id: model.id, name: model.name ?? model.id, provider: PROVIDER, api: protocol,
21
- baseUrl: captured.baseURL, reasoning: !!efforts, ...(thinkingLevelMap ? { thinkingLevelMap } : {}),
22
- input: model.inputModalities ?? model.input ?? ['text'], contextWindow: model.contextWindow,
23
- maxTokens: model.maxTokens, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
24
- compat: { ...captured.policy?.compat, ...model.compat, supportsStrictMode: true },
25
- }
26
- })
27
- const piProvider = createProvider({
28
- id: PROVIDER, name: PROVIDER, baseUrl: captured.baseURL, models,
29
- auth: { apiKey: { name: 'DSH key resolver', resolve: async ({ credential }) => ({ auth: { apiKey: credential?.key ?? 'cpa-keyless' }, source: 'DSH' }) } },
30
- api: createTransport(protocol, captured.baseURL, diagnostics, globalThis.fetch, captured.authIdentity?.type === 'none'),
31
- })
32
- const profile = {
33
- provider: PROVIDER, displayName: PROVIDER, baseURL: captured.baseURL, piProvider, api: protocol,
34
- authIdentity: captured.authIdentity, headers: captured.policy?.headers,
35
- reasoning: captured.policy?.reasoning, thinkingBudgets: captured.policy?.thinkingBudgets,
36
- cacheRetention: captured.policy?.cacheRetention, transport: captured.policy?.transport,
37
- timeoutMs: captured.policy?.timeoutMs, websocketConnectTimeoutMs: captured.policy?.websocketConnectTimeoutMs,
38
- streamIdleTimeoutMs: captured.policy?.streamIdleTimeoutMs ?? 30000,
39
- maxRequestImageBytes: captured.policy?.maxRequestImageBytes ?? 20 * 1024 * 1024,
40
- maxRequestImages: captured.policy?.maxRequestImages ?? 50,
41
- imageOffloadCountQuantum: captured.policy?.imageOffloadCountQuantum ?? Math.min(10, captured.policy?.maxRequestImages ?? 50),
42
- requestImagePixelBudget: captured.policy?.requestImagePixelBudget ?? 2048 * 2048, requestImageMaxBytes: captured.policy?.requestImageMaxBytes ?? 1024 * 1024,
43
- retryPolicy: resolveRetryPolicy(captured.policy?.retryPolicy, 'CPA'),
44
- modelErrors: new Map(), configuredMaxTokens: new Map(),
45
- }
46
- profiles = new Map([[PROVIDER, profile]])
47
- }
48
- if (snapshot) update(snapshot)
49
- const adapter = new PiAiAdapter({
50
- profiles: () => profiles,
51
- resolveApiKey: async (_provider, profile) => {
52
- const value = await resolveIdentity(ctx.credentials, profile.authIdentity)
53
- if (profile.authIdentity?.type !== 'none' && !value) {
54
- onMissing()
55
- throw new LlmError('CPA credential is missing', 'MISSING_CREDENTIAL')
56
- }
57
- return value
58
- },
59
- auth: { credentials: new InMemoryCredentialStore(), authContext: defaultProviderAuthContext },
60
- resolveAttachments: () => ctx.get('attachments'),
61
- resolveImageAccess: (attachments, ref) => resolveImageAttachmentAccess(attachments, (path) => ctx.get('fs')?.processPathFromHostPath(path), ref),
62
- onReplayDegrade: () => {
63
- diagnostics.push({ at: Date.now(), stage: 'replay-degraded' })
64
- if (diagnostics.length > 50) diagnostics.splice(0, diagnostics.length - 50)
65
- },
66
- })
67
- return { adapter, update }
68
- }
1
+ import { PiAiAdapter } from '@deepseek-ai/dsh-llm-pi-ai'
2
+ import { LlmError, resolveImageAttachmentAccess, resolveRetryPolicy } from '@deepseek-ai/dsh-llm'
3
+ import { createProvider, InMemoryCredentialStore, defaultProviderAuthContext } from '@earendil-works/pi-ai'
4
+ import { createTransport } from './transport.js'
5
+ import { resolveIdentity } from './connection.js'
6
+ import { protocolOf } from './policy.js'
7
+
8
+ const PROVIDER = 'CLIProxyAPI'
9
+ const LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']
10
+
11
+ export function createGatewayAdapter(ctx, snapshot, diagnostics, onMissing = () => { }) {
12
+ let profiles = new Map()
13
+ const update = (next) => {
14
+ const captured = structuredClone(next)
15
+ const protocol = protocolOf(captured.policy)
16
+ const models = captured.models.filter((model) => captured.modelSelection?.[model.id] !== false).map((model) => {
17
+ const efforts = model.reasoningEfforts
18
+ const thinkingLevelMap = efforts && Object.fromEntries(LEVELS.flatMap((level) => efforts[level] === undefined ? [[level, null]] : efforts[level] === null ? [] : [[level, efforts[level]]]))
19
+ return {
20
+ id: model.id, name: model.name ?? model.id, provider: PROVIDER, api: protocol,
21
+ baseUrl: captured.baseURL, reasoning: !!efforts, ...(thinkingLevelMap ? { thinkingLevelMap } : {}),
22
+ input: model.inputModalities ?? model.input ?? ['text'], contextWindow: model.contextWindow,
23
+ maxTokens: model.maxTokens, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
24
+ compat: { ...captured.policy?.compat, ...model.compat, supportsStrictMode: true },
25
+ }
26
+ })
27
+ const piProvider = createProvider({
28
+ id: PROVIDER, name: PROVIDER, baseUrl: captured.baseURL, models,
29
+ auth: { apiKey: { name: 'DSH key resolver', resolve: async ({ credential }) => ({ auth: { apiKey: credential?.key ?? 'cpa-keyless' }, source: 'DSH' }) } },
30
+ api: createTransport(protocol, captured.baseURL, diagnostics, globalThis.fetch, captured.authIdentity?.type === 'none'),
31
+ })
32
+ const profile = {
33
+ provider: PROVIDER, displayName: PROVIDER, baseURL: captured.baseURL, piProvider, api: protocol,
34
+ authIdentity: captured.authIdentity, headers: captured.policy?.headers,
35
+ reasoning: captured.policy?.reasoning, thinkingBudgets: captured.policy?.thinkingBudgets,
36
+ cacheRetention: captured.policy?.cacheRetention, transport: captured.policy?.transport,
37
+ timeoutMs: captured.policy?.timeoutMs, websocketConnectTimeoutMs: captured.policy?.websocketConnectTimeoutMs,
38
+ streamIdleTimeoutMs: captured.policy?.streamIdleTimeoutMs ?? 30000,
39
+ maxRequestImageBytes: captured.policy?.maxRequestImageBytes ?? 20 * 1024 * 1024,
40
+ maxRequestImages: captured.policy?.maxRequestImages ?? 50,
41
+ imageOffloadCountQuantum: captured.policy?.imageOffloadCountQuantum ?? Math.min(10, captured.policy?.maxRequestImages ?? 50),
42
+ requestImagePixelBudget: captured.policy?.requestImagePixelBudget ?? 2048 * 2048, requestImageMaxBytes: captured.policy?.requestImageMaxBytes ?? 1024 * 1024,
43
+ retryPolicy: resolveRetryPolicy(captured.policy?.retryPolicy, 'CPA'),
44
+ modelErrors: new Map(), configuredMaxTokens: new Map(),
45
+ }
46
+ profiles = new Map([[PROVIDER, profile]])
47
+ }
48
+ if (snapshot) update(snapshot)
49
+ const adapter = new PiAiAdapter({
50
+ profiles: () => profiles,
51
+ resolveApiKey: async (_provider, profile) => {
52
+ const value = await resolveIdentity(ctx.credentials, profile.authIdentity)
53
+ if (profile.authIdentity?.type !== 'none' && !value) {
54
+ onMissing()
55
+ throw new LlmError('CPA credential is missing', 'MISSING_CREDENTIAL')
56
+ }
57
+ return value
58
+ },
59
+ auth: { credentials: new InMemoryCredentialStore(), authContext: defaultProviderAuthContext },
60
+ resolveAttachments: () => ctx.get('attachments'),
61
+ resolveImageAccess: (attachments, ref) => resolveImageAttachmentAccess(attachments, (path) => ctx.get('fs')?.processPathFromHostPath(path), ref),
62
+ onReplayDegrade: () => {
63
+ diagnostics.push({ at: Date.now(), stage: 'replay-degraded' })
64
+ if (diagnostics.length > 50) diagnostics.splice(0, diagnostics.length - 50)
65
+ },
66
+ })
67
+ return { adapter, update }
68
+ }
@@ -1,62 +1,62 @@
1
- import { createHash, randomUUID } from 'node:crypto'
2
- import { mkdir, open, rename, unlink, writeFile } from 'node:fs/promises'
3
- import { dirname } from 'node:path'
4
- import { dshCachePath } from '@deepseek-ai/dsh-home-paths'
5
-
6
- const MAX_CACHE_BYTES = 8 * 1024 * 1024
7
- const MAX_CATALOG_BYTES = 4 * 1024 * 1024
8
-
9
- function cachePath(documentPath, generation) {
10
- if (typeof documentPath !== 'string' || !documentPath || typeof generation !== 'string' || !/^[a-f0-9]{64}$/.test(generation)) throw new TypeError('A document path and valid generation are required')
11
- const documentHash = createHash('sha256').update(documentPath).digest('hex')
12
- return dshCachePath('llm-cliproxyapi', documentHash, `${generation}.json`)
13
- }
14
-
15
- function redact(value) {
16
- if (Array.isArray(value)) return value.map(redact)
17
- if (!value || typeof value !== 'object') return value
18
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, /^(?:api[_-]?key|authorization|proxy[_-]?authorization|cookie|set-cookie|password|secret|access[_-]?token|refresh[_-]?token)$/i.test(key) ? '[redacted]' : redact(item)]))
19
- }
20
- function validModels(models) {
21
- return Array.isArray(models) && models.length > 0 && models.every((model) => model && typeof model === 'object' && !Array.isArray(model) && typeof model.id === 'string' && model.id.length > 0 && typeof model.name === 'string' && Number.isSafeInteger(model.contextWindow) && model.contextWindow > 0 && Number.isSafeInteger(model.maxTokens) && model.maxTokens > 0 && Array.isArray(model.inputModalities) && model.inputModalities.length > 0 && model.inputModalities.every((input) => input === 'text' || input === 'image') && (model.reasoningEfforts === undefined || (model.reasoningEfforts && typeof model.reasoningEfforts === 'object' && !Array.isArray(model.reasoningEfforts) && Object.entries(model.reasoningEfforts).every(([level, wire]) => ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'].includes(level) && (typeof wire === 'string' && wire.length > 0 || level === 'off' && wire === null))))) && new Set(models.map((model) => model.id)).size === models.length
22
- }
23
-
24
- export async function readCatalogCache(documentPath, generation, baseURL, authIdentity) {
25
- try {
26
- const handle = await open(cachePath(documentPath, generation), 'r')
27
- let data
28
- try {
29
- const buffer = Buffer.alloc(MAX_CACHE_BYTES + 1)
30
- const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0)
31
- if (bytesRead > MAX_CACHE_BYTES) return undefined
32
- data = buffer.subarray(0, bytesRead)
33
- } finally {
34
- await handle.close()
35
- }
36
- const value = JSON.parse(data.toString('utf8'))
37
- if (!value || value.schemaVersion !== 1 || value.generation !== generation || value.baseURL !== baseURL || JSON.stringify(value.authIdentity) !== JSON.stringify(authIdentity) || !Number.isFinite(value.fetchedAt) || !Array.isArray(value.raw) || !validModels(value.effective)) return undefined
38
- return value
39
- } catch (error) {
40
- if (error.code === 'ENOENT' || error instanceof SyntaxError) return undefined
41
- throw error
42
- }
43
- }
44
-
45
- export async function writeCatalogCache(documentPath, snapshot) {
46
- const file = cachePath(documentPath, snapshot.generation)
47
- const raw = redact(snapshot.raw)
48
- const effective = redact(snapshot.effective)
49
- if (!Array.isArray(raw) || !validModels(effective)) throw new TypeError('Catalog models are invalid')
50
- if (Buffer.byteLength(JSON.stringify(raw)) > MAX_CATALOG_BYTES) throw new RangeError('Catalog exceeds 4 MiB')
51
- const contents = JSON.stringify({ schemaVersion: 1, generation: snapshot.generation, baseURL: snapshot.baseURL, authIdentity: snapshot.authIdentity, fetchedAt: snapshot.fetchedAt, raw, effective })
52
- if (Buffer.byteLength(contents) > MAX_CACHE_BYTES) throw new RangeError('Catalog cache exceeds 8 MiB')
53
- await mkdir(dirname(file), { recursive: true })
54
- const temporary = `${file}.${randomUUID()}.tmp`
55
- try {
56
- await writeFile(temporary, contents, { encoding: 'utf8', flag: 'wx', mode: 0o600 })
57
- await rename(temporary, file)
58
- } catch (error) {
59
- await unlink(temporary).catch(() => { })
60
- throw error
61
- }
62
- }
1
+ import { createHash, randomUUID } from 'node:crypto'
2
+ import { mkdir, open, rename, unlink, writeFile } from 'node:fs/promises'
3
+ import { dirname } from 'node:path'
4
+ import { dshCachePath } from '@deepseek-ai/dsh-home-paths'
5
+
6
+ const MAX_CACHE_BYTES = 8 * 1024 * 1024
7
+ const MAX_CATALOG_BYTES = 4 * 1024 * 1024
8
+
9
+ function cachePath(documentPath, generation) {
10
+ if (typeof documentPath !== 'string' || !documentPath || typeof generation !== 'string' || !/^[a-f0-9]{64}$/.test(generation)) throw new TypeError('A document path and valid generation are required')
11
+ const documentHash = createHash('sha256').update(documentPath).digest('hex')
12
+ return dshCachePath('llm-cliproxyapi', documentHash, `${generation}.json`)
13
+ }
14
+
15
+ function redact(value) {
16
+ if (Array.isArray(value)) return value.map(redact)
17
+ if (!value || typeof value !== 'object') return value
18
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, /^(?:api[_-]?key|authorization|proxy[_-]?authorization|cookie|set-cookie|password|secret|access[_-]?token|refresh[_-]?token)$/i.test(key) ? '[redacted]' : redact(item)]))
19
+ }
20
+ function validModels(models) {
21
+ return Array.isArray(models) && models.length > 0 && models.every((model) => model && typeof model === 'object' && !Array.isArray(model) && typeof model.id === 'string' && model.id.length > 0 && typeof model.name === 'string' && Number.isSafeInteger(model.contextWindow) && model.contextWindow > 0 && Number.isSafeInteger(model.maxTokens) && model.maxTokens > 0 && Array.isArray(model.inputModalities) && model.inputModalities.length > 0 && model.inputModalities.every((input) => input === 'text' || input === 'image') && (model.reasoningEfforts === undefined || (model.reasoningEfforts && typeof model.reasoningEfforts === 'object' && !Array.isArray(model.reasoningEfforts) && Object.entries(model.reasoningEfforts).every(([level, wire]) => ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'].includes(level) && (typeof wire === 'string' && wire.length > 0 || level === 'off' && wire === null))))) && new Set(models.map((model) => model.id)).size === models.length
22
+ }
23
+
24
+ export async function readCatalogCache(documentPath, generation, baseURL, authIdentity) {
25
+ try {
26
+ const handle = await open(cachePath(documentPath, generation), 'r')
27
+ let data
28
+ try {
29
+ const buffer = Buffer.alloc(MAX_CACHE_BYTES + 1)
30
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0)
31
+ if (bytesRead > MAX_CACHE_BYTES) return undefined
32
+ data = buffer.subarray(0, bytesRead)
33
+ } finally {
34
+ await handle.close()
35
+ }
36
+ const value = JSON.parse(data.toString('utf8'))
37
+ if (!value || value.schemaVersion !== 1 || value.generation !== generation || value.baseURL !== baseURL || JSON.stringify(value.authIdentity) !== JSON.stringify(authIdentity) || !Number.isFinite(value.fetchedAt) || !Array.isArray(value.raw) || !validModels(value.effective)) return undefined
38
+ return value
39
+ } catch (error) {
40
+ if (error.code === 'ENOENT' || error instanceof SyntaxError) return undefined
41
+ throw error
42
+ }
43
+ }
44
+
45
+ export async function writeCatalogCache(documentPath, snapshot) {
46
+ const file = cachePath(documentPath, snapshot.generation)
47
+ const raw = redact(snapshot.raw)
48
+ const effective = redact(snapshot.effective)
49
+ if (!Array.isArray(raw) || !validModels(effective)) throw new TypeError('Catalog models are invalid')
50
+ if (Buffer.byteLength(JSON.stringify(raw)) > MAX_CATALOG_BYTES) throw new RangeError('Catalog exceeds 4 MiB')
51
+ const contents = JSON.stringify({ schemaVersion: 1, generation: snapshot.generation, baseURL: snapshot.baseURL, authIdentity: snapshot.authIdentity, fetchedAt: snapshot.fetchedAt, raw, effective })
52
+ if (Buffer.byteLength(contents) > MAX_CACHE_BYTES) throw new RangeError('Catalog cache exceeds 8 MiB')
53
+ await mkdir(dirname(file), { recursive: true })
54
+ const temporary = `${file}.${randomUUID()}.tmp`
55
+ try {
56
+ await writeFile(temporary, contents, { encoding: 'utf8', flag: 'wx', mode: 0o600 })
57
+ await rename(temporary, file)
58
+ } catch (error) {
59
+ await unlink(temporary).catch(() => { })
60
+ throw error
61
+ }
62
+ }