@thebtf/dsh-cliproxyapi-provider 0.0.0-stage → 0.4.3
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/README.md +39 -3
- package/README_EN.md +39 -0
- package/client.js +517 -0
- package/cordis.patch.yml +5 -0
- package/details/en.md +9 -0
- package/details/ru.md +9 -0
- package/details/zh.md +9 -0
- package/locale/en.json +7 -0
- package/locale/ru.json +7 -0
- package/locale/zh.json +7 -0
- package/package.json +85 -6
- package/src/adapter.js +68 -0
- package/src/catalog-store.js +62 -0
- package/src/catalog.js +129 -0
- package/src/connection-store.js +98 -0
- package/src/connection.js +114 -0
- package/src/gateway.js +411 -0
- package/src/index.js +513 -0
- package/src/migration.js +132 -0
- package/src/policy.js +42 -0
- package/src/secrets.js +1 -0
- package/src/transport.js +114 -0
package/src/policy.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { resolveRetryPolicy } from '@deepseek-ai/dsh-llm'
|
|
2
|
+
import { Config as NativePiAiConfig } from '@deepseek-ai/dsh-llm-pi-ai'
|
|
3
|
+
import { ConnectionError } from './connection.js'
|
|
4
|
+
import { credentialField } from './secrets.js'
|
|
5
|
+
|
|
6
|
+
// The wire protocols this build can serve. The order is the presentation order:
|
|
7
|
+
// the first entry is the default a fresh connection starts with.
|
|
8
|
+
export const PROTOCOLS = ['openai-responses', 'openai-completions', 'anthropic-messages', 'google-generative-ai']
|
|
9
|
+
export const DEFAULT_PROTOCOL = PROTOCOLS[0]
|
|
10
|
+
|
|
11
|
+
const fields = new Set(['displayName', 'api', 'defaultContextWindow', 'defaultMaxTokens', 'defaultInput', 'headers', 'compat', 'retryPolicy', 'reasoning', 'thinkingBudgets', 'streamIdleTimeoutMs', 'maxRequestImageBytes', 'maxRequestImages', 'imageOffloadCountQuantum', 'requestImagePixelBudget', 'requestImageMaxBytes', 'cacheRetention', 'transport', 'timeoutMs', 'websocketConnectTimeoutMs'])
|
|
12
|
+
const object = (v) => v !== null && typeof v === 'object' && !Array.isArray(v)
|
|
13
|
+
const containsCredentials = (v) => object(v) && Object.entries(v).some(([k, x]) => credentialField(k) || containsCredentials(x)) || Array.isArray(v) && v.some(containsCredentials)
|
|
14
|
+
export function protocolOf(policy) {
|
|
15
|
+
return PROTOCOLS.includes(policy?.api) ? policy.api : DEFAULT_PROTOCOL
|
|
16
|
+
}
|
|
17
|
+
export function validatePolicy(policy = {}) {
|
|
18
|
+
try {
|
|
19
|
+
if (!object(policy) || containsCredentials(policy) || Object.keys(policy).some((k) => !fields.has(k))) throw 0
|
|
20
|
+
for (const key of ['defaultContextWindow', 'defaultMaxTokens', 'maxRequestImageBytes', 'maxRequestImages', 'imageOffloadCountQuantum', 'requestImagePixelBudget', 'requestImageMaxBytes']) if (policy[key] !== undefined && (!Number.isSafeInteger(policy[key]) || policy[key] <= 0)) throw 0
|
|
21
|
+
for (const key of ['timeoutMs', 'websocketConnectTimeoutMs']) if (policy[key] !== undefined && (!Number.isSafeInteger(policy[key]) || policy[key] < 0)) throw 0
|
|
22
|
+
if (policy.streamIdleTimeoutMs !== undefined && (!Number.isFinite(policy.streamIdleTimeoutMs) || policy.streamIdleTimeoutMs <= 0 || policy.streamIdleTimeoutMs > 2147483647)) throw 0
|
|
23
|
+
if (policy.imageOffloadCountQuantum > (policy.maxRequestImages ?? 50)) throw 0
|
|
24
|
+
if (policy.defaultInput !== undefined && (!Array.isArray(policy.defaultInput) || !policy.defaultInput.length || policy.defaultInput.some((x) => !['text', 'image'].includes(x)))) throw 0
|
|
25
|
+
if (policy.headers !== undefined && (!object(policy.headers) || Object.values(policy.headers).some((x) => typeof x !== 'string'))) throw 0
|
|
26
|
+
if (policy.compat !== undefined && !object(policy.compat) || policy.thinkingBudgets !== undefined && (!object(policy.thinkingBudgets) || Object.values(policy.thinkingBudgets).some((x) => !Number.isSafeInteger(x) || x < 0))) throw 0
|
|
27
|
+
if (policy.cacheRetention !== undefined && !['none', 'short', 'long'].includes(policy.cacheRetention)) throw 0
|
|
28
|
+
if (policy.transport !== undefined && !['auto', 'sse', 'websocket', 'websocket-cached'].includes(policy.transport)) throw 0
|
|
29
|
+
if (policy.reasoning !== undefined && !['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'].includes(policy.reasoning)) throw 0
|
|
30
|
+
if (policy.displayName !== undefined && typeof policy.displayName !== 'string' || policy.api !== undefined && !PROTOCOLS.includes(policy.api)) throw 0
|
|
31
|
+
if (policy.retryPolicy !== undefined && (!object(policy.retryPolicy) || policy.retryPolicy.backoff !== undefined && !object(policy.retryPolicy.backoff) || policy.retryPolicy.retryableCodes !== undefined && !Array.isArray(policy.retryPolicy.retryableCodes))) throw 0
|
|
32
|
+
resolveRetryPolicy(policy.retryPolicy, 'CPA')
|
|
33
|
+
const checked = NativePiAiConfig['~standard'].validate({
|
|
34
|
+
providers: { 'cpa-policy-validation': { ...policy, api: protocolOf(policy), baseURL: 'https://policy.invalid/v1', models: [{ id: 'policy-validation' }] } },
|
|
35
|
+
})
|
|
36
|
+
if (!checked || typeof checked.then === 'function' || checked.issues) throw 0
|
|
37
|
+
const parsed = checked.value.providers.get()['cpa-policy-validation']
|
|
38
|
+
for (const [key, value] of Object.entries(policy.compat ?? {})) {
|
|
39
|
+
if (JSON.stringify(parsed.compat?.[key]) !== JSON.stringify(value)) throw 0
|
|
40
|
+
}
|
|
41
|
+
} catch { throw new ConnectionError('INVALID_POLICY', 'Invalid or unsupported public policy') }
|
|
42
|
+
}
|
package/src/secrets.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const credentialField = (name) => /^(?:(?:x[-_])?api[-_]?key|(?:[\w]+[-_])*authorization|cookie|set[-_]?cookie|password|secret|token|key|access[-_]?token|refresh[-_]?token|credential)$/i.test(name)
|
package/src/transport.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { openAIResponsesApi } from '@earendil-works/pi-ai/api/openai-responses.lazy'
|
|
2
|
+
import { openAICompletionsApi } from '@earendil-works/pi-ai/api/openai-completions.lazy'
|
|
3
|
+
import { anthropicMessagesApi } from '@earendil-works/pi-ai/api/anthropic-messages.lazy'
|
|
4
|
+
import { googleGenerativeAIApi } from '@earendil-works/pi-ai/api/google-generative-ai.lazy'
|
|
5
|
+
import { PROTOCOLS } from './policy.js'
|
|
6
|
+
|
|
7
|
+
const FACTORIES = {
|
|
8
|
+
'openai-responses': openAIResponsesApi,
|
|
9
|
+
'openai-completions': openAICompletionsApi,
|
|
10
|
+
'anthropic-messages': anthropicMessagesApi,
|
|
11
|
+
'google-generative-ai': googleGenerativeAIApi,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Each wire protocol ends its SSE stream differently. The scanner only feeds the
|
|
15
|
+
// diagnostics panel, so an unknown protocol degrades to "no terminal event seen"
|
|
16
|
+
// instead of guessing.
|
|
17
|
+
const TERMINALS = {
|
|
18
|
+
'openai-responses': (line) => {
|
|
19
|
+
const match = /^(?:event:\s*|data:\s*\{"type"\s*:\s*")(response\.(?:completed|failed|incomplete))/.exec(line)
|
|
20
|
+
return match ? match[1].replace('response.', 'sse-') : undefined
|
|
21
|
+
},
|
|
22
|
+
'openai-completions': (line) => {
|
|
23
|
+
if (/^data:\s*\[DONE\]/.test(line)) return 'sse-completed'
|
|
24
|
+
return /^data:\s*\{/.test(line) && /"error"\s*:/.test(line) ? 'sse-failed' : undefined
|
|
25
|
+
},
|
|
26
|
+
'anthropic-messages': (line) => {
|
|
27
|
+
const match = /^(?:event:\s*|data:\s*\{[^\n]*"type"\s*:\s*")(message_stop|error)/.exec(line)
|
|
28
|
+
if (!match) return undefined
|
|
29
|
+
return match[1] === 'error' ? 'sse-failed' : 'sse-completed'
|
|
30
|
+
},
|
|
31
|
+
'google-generative-ai': (line) => {
|
|
32
|
+
const match = /^(?:event:\s*|data:\s*\{[^\n]*"type"\s*:\s*")(result|error)/.exec(line)
|
|
33
|
+
if (!match) return undefined
|
|
34
|
+
return match[1] === 'error' ? 'sse-failed' : 'sse-completed'
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function resolveProtocol(protocol) {
|
|
39
|
+
return PROTOCOLS.includes(protocol) ? protocol : 'openai-responses'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function terminalStage(protocol, line) {
|
|
43
|
+
return TERMINALS[resolveProtocol(protocol)](line)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createTransport(protocol, baseURL, diagnostics, fetchImpl = globalThis.fetch, keyless = false) {
|
|
47
|
+
const api = resolveProtocol(protocol)
|
|
48
|
+
const origin = new URL(baseURL).origin
|
|
49
|
+
const native = FACTORIES[api]()
|
|
50
|
+
const terminalOf = TERMINALS[api]
|
|
51
|
+
// Google streams carry no sentinel, so a clean flush is a completed stream.
|
|
52
|
+
const flushStage = api === 'google-generative-ai' ? 'sse-completed' : 'sse-incomplete'
|
|
53
|
+
const record = (entry) => {
|
|
54
|
+
diagnostics.push({ at: Date.now(), ...entry })
|
|
55
|
+
if (diagnostics.length > 50) diagnostics.splice(0, diagnostics.length - 50)
|
|
56
|
+
}
|
|
57
|
+
const scopedFetch = async (input, init = {}) => {
|
|
58
|
+
const url = input instanceof Request ? input.url : String(input)
|
|
59
|
+
if (new URL(url).origin !== origin) throw new Error('CPA request origin changed')
|
|
60
|
+
try {
|
|
61
|
+
const headers = new Headers(input instanceof Request ? input.headers : undefined)
|
|
62
|
+
if (init.headers) new Headers(init.headers).forEach((value, key) => headers.set(key, value))
|
|
63
|
+
if (keyless) headers.delete('authorization')
|
|
64
|
+
const response = await fetchImpl(input, { ...init, headers, redirect: 'error' })
|
|
65
|
+
const requestId = response.headers.get('x-request-id')
|
|
66
|
+
record({ stage: 'http', status: response.status, ...(/^[A-Za-z0-9._:-]{1,128}$/.test(requestId ?? '') ? { requestId } : {}) })
|
|
67
|
+
if (!response.body || !response.headers.get('content-type')?.toLowerCase().includes('text/event-stream')) return response
|
|
68
|
+
let pending = ''
|
|
69
|
+
let terminal = false
|
|
70
|
+
const signal = init.signal ?? (input instanceof Request ? input.signal : undefined)
|
|
71
|
+
const onAbort = () => { if (!terminal) { terminal = true; record({ stage: signal?.reason?.name === 'TimeoutError' ? 'timeout' : 'abort' }) } }
|
|
72
|
+
if (signal?.aborted) onAbort()
|
|
73
|
+
else signal?.addEventListener('abort', onAbort, { once: true })
|
|
74
|
+
const cleanup = () => signal?.removeEventListener('abort', onAbort)
|
|
75
|
+
const decoder = new TextDecoder()
|
|
76
|
+
const inspect = (text) => {
|
|
77
|
+
pending += text
|
|
78
|
+
const lines = pending.split(/\r?\n/)
|
|
79
|
+
pending = lines.pop().slice(-1024)
|
|
80
|
+
for (const line of lines) {
|
|
81
|
+
const stage = terminalOf(line)
|
|
82
|
+
if (stage && !terminal) { terminal = true; cleanup(); record({ stage }) }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const body = response.body.pipeThrough(new TransformStream({
|
|
86
|
+
transform(chunk, controller) { inspect(decoder.decode(chunk, { stream: true })); controller.enqueue(chunk) },
|
|
87
|
+
flush() { inspect(decoder.decode() + '\n'); cleanup(); if (!terminal) record({ stage: flushStage }) },
|
|
88
|
+
}))
|
|
89
|
+
return new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers })
|
|
90
|
+
} catch (error) {
|
|
91
|
+
record({ stage: error?.name === 'TimeoutError' ? 'timeout' : error?.name === 'AbortError' ? 'abort' : 'before-http' })
|
|
92
|
+
throw error
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const withPolicy = (options) => ({
|
|
96
|
+
...options,
|
|
97
|
+
maxRetries: 0,
|
|
98
|
+
fetch: scopedFetch,
|
|
99
|
+
onPayload(payload, model) {
|
|
100
|
+
const applyPolicy = (value) => {
|
|
101
|
+
const updated = { ...value }
|
|
102
|
+
if (Array.isArray(value.tools)) updated.tools = value.tools.map((tool) => tool.type === 'function' ? { ...tool, strict: false } : tool)
|
|
103
|
+
record({ stage: 'payload', toolCount: Array.isArray(value.tools) ? value.tools.length : 0 })
|
|
104
|
+
return updated
|
|
105
|
+
}
|
|
106
|
+
const original = options?.onPayload?.(payload, model)
|
|
107
|
+
return original instanceof Promise ? original.then((value) => applyPolicy(value ?? payload)) : applyPolicy(original ?? payload)
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
return {
|
|
111
|
+
stream: (model, context, options) => native.stream(model, context, withPolicy(options)),
|
|
112
|
+
streamSimple: (model, context, options) => native.streamSimple(model, context, withPolicy(options)),
|
|
113
|
+
}
|
|
114
|
+
}
|