@yss123/cage-ai 0.0.0-stage → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -2
  3. package/dist/budget.d.ts +64 -0
  4. package/dist/budget.js +54 -0
  5. package/dist/budget.js.map +1 -0
  6. package/dist/context-read.d.ts +38 -0
  7. package/dist/context-read.js +124 -0
  8. package/dist/context-read.js.map +1 -0
  9. package/dist/context-schema.d.ts +9 -0
  10. package/dist/context-schema.js +16 -0
  11. package/dist/context-schema.js.map +1 -0
  12. package/dist/context.d.ts +27 -0
  13. package/dist/context.js +83 -0
  14. package/dist/context.js.map +1 -0
  15. package/dist/execution-schema.d.ts +9 -0
  16. package/dist/execution-schema.js +20 -0
  17. package/dist/execution-schema.js.map +1 -0
  18. package/dist/history.d.ts +43 -0
  19. package/dist/history.js +139 -0
  20. package/dist/history.js.map +1 -0
  21. package/dist/http.d.ts +15 -0
  22. package/dist/http.js +197 -0
  23. package/dist/http.js.map +1 -0
  24. package/dist/index.d.ts +33 -0
  25. package/dist/index.js +100 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/model-failure.d.ts +66 -0
  28. package/dist/model-failure.js +85 -0
  29. package/dist/model-failure.js.map +1 -0
  30. package/dist/model.d.ts +50 -0
  31. package/dist/model.js +93 -0
  32. package/dist/model.js.map +1 -0
  33. package/dist/models.d.ts +75 -0
  34. package/dist/models.js +241 -0
  35. package/dist/models.js.map +1 -0
  36. package/dist/personalities.d.ts +87 -0
  37. package/dist/personalities.js +191 -0
  38. package/dist/personalities.js.map +1 -0
  39. package/dist/personality-presets.d.ts +14 -0
  40. package/dist/personality-presets.js +24 -0
  41. package/dist/personality-presets.js.map +1 -0
  42. package/dist/prompt.d.ts +67 -0
  43. package/dist/prompt.js +148 -0
  44. package/dist/prompt.js.map +1 -0
  45. package/dist/query.d.ts +13 -0
  46. package/dist/query.js +82 -0
  47. package/dist/query.js.map +1 -0
  48. package/dist/request-plan.d.ts +59 -0
  49. package/dist/request-plan.js +168 -0
  50. package/dist/request-plan.js.map +1 -0
  51. package/dist/responses.d.ts +29 -0
  52. package/dist/responses.js +134 -0
  53. package/dist/responses.js.map +1 -0
  54. package/dist/runtime.d.ts +172 -0
  55. package/dist/runtime.js +1189 -0
  56. package/dist/runtime.js.map +1 -0
  57. package/dist/schema.d.ts +11 -0
  58. package/dist/schema.js +51 -0
  59. package/dist/schema.js.map +1 -0
  60. package/dist/security.d.ts +23 -0
  61. package/dist/security.js +73 -0
  62. package/dist/security.js.map +1 -0
  63. package/dist/state.d.ts +238 -0
  64. package/dist/state.js +11 -0
  65. package/dist/state.js.map +1 -0
  66. package/dist/store.d.ts +113 -0
  67. package/dist/store.js +456 -0
  68. package/dist/store.js.map +1 -0
  69. package/dist/text-stream.d.ts +62 -0
  70. package/dist/text-stream.js +99 -0
  71. package/dist/text-stream.js.map +1 -0
  72. package/dist/tool-catalog.d.ts +23 -0
  73. package/dist/tool-catalog.js +53 -0
  74. package/dist/tool-catalog.js.map +1 -0
  75. package/dist/tools.d.ts +54 -0
  76. package/dist/tools.js +259 -0
  77. package/dist/tools.js.map +1 -0
  78. package/dist/types.d.ts +283 -0
  79. package/dist/types.js +16 -0
  80. package/dist/types.js.map +1 -0
  81. package/package.json +41 -3
@@ -0,0 +1,66 @@
1
+ /**
2
+ * 模型返回已到达但不能作为完整结果使用的封闭原因集合;区分厂商拒绝、输出截断、结构/参数错误和终态不一致,便于诊断预算与协议问题。
3
+ */
4
+ type ResponseFailure = 'provider_error' | 'incomplete' | 'output_limit' | 'invalid_json' | 'invalid_arguments' | 'invalid_output' | 'invalid_terminal' | 'unexpected_tool' | 'text_mismatch';
5
+ /**
6
+ * 运行记录可安全保存的故障摘要。
7
+ * 可选字段只在异常提供相应证据时出现;没有证据不能推断 HTTP 状态、网络错误或厂商停止原因。
8
+ */
9
+ export interface ModelFailure {
10
+ /**
11
+ * 稳定的细分原因;未知错误仅记 request_failed。
12
+ */
13
+ reason: string;
14
+ /**
15
+ * 可选框架错误码,用于界面与诊断分类。
16
+ */
17
+ code?: string;
18
+ /**
19
+ * 连接器实际取得的 HTTP 状态;没有响应时不填。
20
+ */
21
+ httpStatus?: number;
22
+ /**
23
+ * 经过字符白名单过滤的上游错误代码,不是上游 message。
24
+ */
25
+ providerCode?: string;
26
+ /**
27
+ * 经过白名单过滤的生成终止原因,例如输出长度限制。
28
+ */
29
+ stopReason?: string;
30
+ /**
31
+ * 允许列表内的网络或 TLS 代码;不记录任意 cause 内容。
32
+ */
33
+ networkCode?: string;
34
+ }
35
+ /** 只允许稳定代码进入运行记录;上游 message、正文和解析器片段可能含凭据或业务内容。 */
36
+ export declare function providerErrorCode(value: unknown): string | undefined;
37
+ /**
38
+ * 统一表达模型协议错误,并把原因映射成稳定 AI_MODEL_* 错误码。
39
+ * 错误消息只含框架代码;details 用于携带可筛选的厂商代码和终止原因,不接收原始响应正文。
40
+ */
41
+ export declare class ModelResponseError extends Error {
42
+ readonly reason: ResponseFailure;
43
+ readonly details: {
44
+ providerCode?: string;
45
+ stopReason?: string;
46
+ };
47
+ /**
48
+ * 根据 reason 映射出的稳定框架错误码。
49
+ */
50
+ readonly code: string;
51
+ /**
52
+ * 根据输出上限、不完整、厂商失败或其余协议错误选择稳定代码,保留具体 reason 供运行时诊断;不触发请求重试。
53
+ */
54
+ constructor(reason: ResponseFailure, details?: {
55
+ providerCode?: string;
56
+ stopReason?: string;
57
+ });
58
+ }
59
+ /**
60
+ * 解析模型 JSON 或函数参数 JSON;解析失败抛带指定原因的统一异常。
61
+ * 刻意不转发 JSON.parse 的错误片段,避免模型正文或业务数据进入日志。返回值仍需后续结构 schema 校验。
62
+ */
63
+ export declare function parseModelJson(value: string, reason: 'invalid_json' | 'invalid_arguments'): any;
64
+ /** 分类不改变重试策略;没有证据时保留未知,不能把所有失败都叫超时或网络中断。 */
65
+ export declare function modelFailure(error: unknown): ModelFailure;
66
+ export {};
@@ -0,0 +1,85 @@
1
+ /**
2
+ * 把上游协议、连接器和网络异常转为可持久化的模型故障分类。
3
+ * 模型适配器负责抛出协议错误,运行时用这里的稳定代码记录事件;不保留原始错误正文、参数碎片或凭据,不在此决定重试。
4
+ */
5
+ import { ConnectorError } from '@yss123/cage-connectors';
6
+ /** 只允许稳定代码进入运行记录;上游 message、正文和解析器片段可能含凭据或业务内容。 */
7
+ export function providerErrorCode(value) {
8
+ return typeof value === 'string' && /^[A-Za-z0-9_.-]{1,100}$/.test(value) ? value : undefined;
9
+ }
10
+ /**
11
+ * 统一表达模型协议错误,并把原因映射成稳定 AI_MODEL_* 错误码。
12
+ * 错误消息只含框架代码;details 用于携带可筛选的厂商代码和终止原因,不接收原始响应正文。
13
+ */
14
+ export class ModelResponseError extends Error {
15
+ reason;
16
+ details;
17
+ /**
18
+ * 根据 reason 映射出的稳定框架错误码。
19
+ */
20
+ code;
21
+ /**
22
+ * 根据输出上限、不完整、厂商失败或其余协议错误选择稳定代码,保留具体 reason 供运行时诊断;不触发请求重试。
23
+ */
24
+ constructor(reason, details = {}) {
25
+ const code = reason === 'output_limit'
26
+ ? 'AI_MODEL_OUTPUT_LIMIT'
27
+ : reason === 'incomplete'
28
+ ? 'AI_MODEL_INCOMPLETE'
29
+ : reason === 'provider_error'
30
+ ? 'AI_MODEL_UPSTREAM_ERROR'
31
+ : 'AI_MODEL_RESPONSE_INVALID';
32
+ super(code);
33
+ this.reason = reason;
34
+ this.details = details;
35
+ this.name = 'ModelResponseError';
36
+ this.code = code;
37
+ }
38
+ }
39
+ /**
40
+ * 解析模型 JSON 或函数参数 JSON;解析失败抛带指定原因的统一异常。
41
+ * 刻意不转发 JSON.parse 的错误片段,避免模型正文或业务数据进入日志。返回值仍需后续结构 schema 校验。
42
+ */
43
+ export function parseModelJson(value, reason) {
44
+ try {
45
+ return JSON.parse(value);
46
+ }
47
+ catch {
48
+ throw new ModelResponseError(reason);
49
+ }
50
+ }
51
+ /** 分类不改变重试策略;没有证据时保留未知,不能把所有失败都叫超时或网络中断。 */
52
+ export function modelFailure(error) {
53
+ if (error instanceof ModelResponseError)
54
+ return {
55
+ code: error.code,
56
+ reason: error.reason,
57
+ providerCode: providerErrorCode(error.details.providerCode),
58
+ stopReason: providerErrorCode(error.details.stopReason),
59
+ };
60
+ if (error instanceof ConnectorError)
61
+ return {
62
+ code: error.status === 429
63
+ ? 'AI_MODEL_RATE_LIMITED'
64
+ : error.status && error.status >= 400
65
+ ? 'AI_MODEL_HTTP_ERROR'
66
+ : 'AI_MODEL_CONNECTION_FAILED',
67
+ reason: error.status && error.status >= 400 ? 'http_error' : 'transport_error',
68
+ ...(error.status !== null ? { httpStatus: error.status } : {}),
69
+ providerCode: providerErrorCode(error.providerCode),
70
+ };
71
+ if (error instanceof Error) {
72
+ if (error.name === 'TimeoutError')
73
+ return { code: 'AI_MODEL_TIMEOUT', reason: 'request_timeout' };
74
+ if (error.name === 'AbortError')
75
+ return { code: 'AI_MODEL_INTERRUPTED', reason: 'request_aborted' };
76
+ const cause = error.cause;
77
+ const value = cause && typeof cause === 'object' && 'code' in cause ? cause.code : undefined;
78
+ const networkCode = providerErrorCode(value);
79
+ if (networkCode &&
80
+ /^(?:E(?:CONNRESET|CONNREFUSED|TIMEDOUT|HOSTUNREACH|NETUNREACH|AI_AGAIN|NOTFOUND|PIPE)|UND_ERR_[A-Z_]+|CERT_[A-Z_]+|ERR_TLS_[A-Z_]+)$/.test(networkCode))
81
+ return { code: 'AI_MODEL_CONNECTION_FAILED', reason: 'network_error', networkCode };
82
+ }
83
+ return { reason: 'request_failed' };
84
+ }
85
+ //# sourceMappingURL=model-failure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-failure.js","sourceRoot":"","sources":["../src/model-failure.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * 将 OpenAI 兼容 Chat Completions SSE 转换为框架统一的模型结果。
3
+ * 由 ai 声明或模型管理器创建 provider,实际网络请求复用连接器的主机、密钥、超时与限速边界;这里只解析模型输出,不执行工具,也不把生成文本当作业务成功。
4
+ */
5
+ import { type ConnectorDef } from '@yss123/cage-connectors';
6
+ import type { ModelProvider, ModelRequest, ModelResult } from './types.js';
7
+ /**
8
+ * 验证真实或 fake provider 的统一输出,并拒绝重复工具调用 ID。
9
+ * 返回经过 schema 解析的数据;结构错误统一抛 ModelResponseError,避免原始模型正文随解析诊断泄漏。该检查不执行工具,也不验证用量是否为厂商真实计费。
10
+ */
11
+ export declare function validateModelResult(value: unknown): ModelResult;
12
+ /**
13
+ * Chat 协议 provider 的声明配置。
14
+ * 主机按连接器环境选择,凭据只在服务端发请求时解析;默认上下文窗口供请求预算使用,不代表网关实际限制已被探测。
15
+ */
16
+ export interface ChatModelOptions {
17
+ /**
18
+ * 网关模型标识,原样写入请求;与 Cage 声明使用的模型别名不同。
19
+ */
20
+ model: string; /** 按 fake/test/prod 环境声明的允许主机,实际出站由连接器检查。 */
21
+ hosts: ConnectorDef<{}>['hosts']; /** 可选的服务端密钥名称;配置中不保存密钥明文。 */
22
+ secret?: string; /** 模型接口绝对路径;省略时使用 Chat Completions 标准路径。 */
23
+ path?: string; /** 连接器请求超时,单位毫秒;省略时为 90000。 */
24
+ timeoutMs?: number;
25
+ /** 模型可独立连接测试服务,不要求 ERP 等其他连接器同时切换环境。省略时沿用 CAGE_STAGE。 */
26
+ environment?: 'fake' | 'test' | 'prod';
27
+ /** 默认 HTTPS;显式声明 HTTP 可连接本地或内网模型网关。 */
28
+ protocol?: 'http:' | 'https:';
29
+ /** 服务端凭据解析器;不进入 spec、注册表、请求正文或日志。 */
30
+ credential?: () => Promise<string>;
31
+ /**
32
+ * 可选每秒请求限额,具体调度由连接器承担。
33
+ */
34
+ rateLimit?: {
35
+ perSecond: number;
36
+ };
37
+ /**
38
+ * 声明的模型总上下文窗口,单位 Token;省略时为 32768,预算估算并非厂商 tokenizer。
39
+ */
40
+ contextWindowTokens?: number;
41
+ /** 本地/CI 不生成看似可信的随机裁决,必须显式提供 fake。 */
42
+ fake?: (request: ModelRequest) => Promise<ModelResult> | ModelResult;
43
+ }
44
+ /** OpenAI-compatible chat/completions 协议适配;不依赖特定厂商或 Agent 框架。 */
45
+ /**
46
+ * 构造延迟执行的 Chat Completions provider,默认路径为 /v1/chat/completions、超时 90 秒。
47
+ * 每次 generate 在 fake 环境要求显式回调;真实请求只发送模型协议消息与工具 schema,按 index 拼接工具参数碎片并逐块等待 onText。
48
+ * 流结束必须同时收到 [DONE] 和 stop/tool_calls 终止原因;截断、无效 JSON 或不完整输出拒绝进入工具执行。返回的 provider 不自行重试请求。
49
+ */
50
+ export declare function chatModel(name: string, options: ChatModelOptions): ModelProvider;
package/dist/model.js ADDED
@@ -0,0 +1,93 @@
1
+ /**
2
+ * 将 OpenAI 兼容 Chat Completions SSE 转换为框架统一的模型结果。
3
+ * 由 ai 声明或模型管理器创建 provider,实际网络请求复用连接器的主机、密钥、超时与限速边界;这里只解析模型输出,不执行工具,也不把生成文本当作业务成功。
4
+ */
5
+ import { connector, stage, streamJson } from '@yss123/cage-connectors';
6
+ import { z } from 'zod';
7
+ import { ModelResponseError, parseModelJson, providerErrorCode } from './model-failure.js';
8
+ /**
9
+ * 限定统一结果的文本、工具调用、用量与远端状态形状。
10
+ * 单轮最多 40 个调用、文本最多 100000 字符;schema 只验证结构,调用名和参数的业务授权由工具目录继续校验。
11
+ */
12
+ const resultSchema = z.object({ text: z.string().max(100_000), calls: z.array(z.object({
13
+ id: z.string().min(1).max(160), name: z.string().min(1).max(160), arguments: z.record(z.string(), z.unknown()),
14
+ }).strict()).max(40), usage: z.object({ inputTokens: z.number().int().nonnegative(), outputTokens: z.number().int().nonnegative() }).optional(), finishReason: z.string().optional(),
15
+ responseId: z.string().min(1).max(512).optional(), outputItems: z.array(z.record(z.string(), z.unknown())).max(100).optional() });
16
+ /**
17
+ * 验证真实或 fake provider 的统一输出,并拒绝重复工具调用 ID。
18
+ * 返回经过 schema 解析的数据;结构错误统一抛 ModelResponseError,避免原始模型正文随解析诊断泄漏。该检查不执行工具,也不验证用量是否为厂商真实计费。
19
+ */
20
+ export function validateModelResult(value) {
21
+ const parsed = resultSchema.safeParse(value);
22
+ if (!parsed.success)
23
+ throw new ModelResponseError('invalid_output');
24
+ const result = parsed.data;
25
+ if (new Set(result.calls.map(call => call.id)).size !== result.calls.length)
26
+ throw new ModelResponseError('invalid_output');
27
+ return result;
28
+ }
29
+ /** OpenAI-compatible chat/completions 协议适配;不依赖特定厂商或 Agent 框架。 */
30
+ /**
31
+ * 构造延迟执行的 Chat Completions provider,默认路径为 /v1/chat/completions、超时 90 秒。
32
+ * 每次 generate 在 fake 环境要求显式回调;真实请求只发送模型协议消息与工具 schema,按 index 拼接工具参数碎片并逐块等待 onText。
33
+ * 流结束必须同时收到 [DONE] 和 stop/tool_calls 终止原因;截断、无效 JSON 或不完整输出拒绝进入工具执行。返回的 provider 不自行重试请求。
34
+ */
35
+ export function chatModel(name, options) {
36
+ const transport = connector(name, { hosts: options.hosts, secret: options.secret, timeoutMs: options.timeoutMs ?? 90_000,
37
+ rateLimit: options.rateLimit, operations: {} });
38
+ const path = options.path ?? '/v1/chat/completions';
39
+ return {
40
+ contextWindowTokens: options.contextWindowTokens ?? 32768,
41
+ spec: { kind: 'chat-completions', name, model: options.model, hosts: options.hosts, secret: options.secret, path, timeoutMs: transport.timeoutMs, environment: options.environment, protocol: options.protocol, contextWindowTokens: options.contextWindowTokens ?? 32768 },
42
+ async generate(request) {
43
+ const environment = options.environment ?? stage();
44
+ if (environment === 'fake') {
45
+ if (!options.fake)
46
+ throw new Error('模型未配置 fake;真实联调需显式选择 test/prod 阶段');
47
+ return validateModelResult(await options.fake(request));
48
+ }
49
+ let text = '', finishReason, usage, done = false;
50
+ const calls = new Map();
51
+ await streamJson(transport, path, { model: options.model, stream: true, stream_options: { include_usage: true },
52
+ max_tokens: request.maxOutputTokens,
53
+ messages: request.messages.map(m => ({ role: m.role, content: m.content || null,
54
+ ...(m.toolCallId ? { tool_call_id: m.toolCallId } : {}),
55
+ ...(m.toolCalls?.length ? { tool_calls: m.toolCalls.map(c => ({ id: c.id, type: 'function', function: { name: c.name, arguments: JSON.stringify(c.arguments) } })) } : {}) })),
56
+ ...(request.tools.length ? { tools: request.tools.map(t => ({ type: 'function', function: { name: t.name, description: t.description, parameters: t.input } })) } : {}),
57
+ }, request.signal, async (data) => {
58
+ if (data === '[DONE]') {
59
+ done = true;
60
+ return;
61
+ }
62
+ const chunk = parseModelJson(data, 'invalid_json');
63
+ if (!chunk || typeof chunk !== 'object')
64
+ throw new ModelResponseError('invalid_output');
65
+ if (chunk.error)
66
+ throw new ModelResponseError('provider_error', { providerCode: providerErrorCode(chunk.error.code) });
67
+ if (chunk.usage)
68
+ usage = { inputTokens: chunk.usage.prompt_tokens, outputTokens: chunk.usage.completion_tokens };
69
+ const choice = chunk.choices?.[0];
70
+ if (choice?.finish_reason)
71
+ finishReason = choice.finish_reason;
72
+ const delta = choice?.delta;
73
+ if (typeof delta?.content === 'string') {
74
+ text += delta.content;
75
+ await request.onText?.(delta.content);
76
+ }
77
+ for (const part of delta?.tool_calls ?? []) {
78
+ if (!Number.isInteger(part.index) || part.index < 0 || part.index >= 40)
79
+ throw new ModelResponseError('invalid_output');
80
+ const call = calls.get(part.index) ?? { id: '', name: '', arguments: '' };
81
+ call.id += part.id ?? '';
82
+ call.name += part.function?.name ?? '';
83
+ call.arguments += part.function?.arguments ?? '';
84
+ calls.set(part.index, call);
85
+ }
86
+ }, { environment, protocol: options.protocol, credential: options.credential });
87
+ if (!done || !finishReason || !['stop', 'tool_calls'].includes(finishReason))
88
+ throw new ModelResponseError(finishReason === 'length' ? 'output_limit' : 'incomplete', { stopReason: providerErrorCode(finishReason) });
89
+ return validateModelResult({ text, finishReason, usage, calls: [...calls.values()].map(call => ({ ...call, arguments: parseModelJson(call.arguments, 'invalid_arguments') })) });
90
+ },
91
+ };
92
+ }
93
+ //# sourceMappingURL=model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.js","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,75 @@
1
+ import { type Ctx, type Kernel } from '@yss123/cage-kernel';
2
+ import { type AiModelConfig, type AiModelProbe } from '@yss123/cage-ai-protocol';
3
+ import type { ModelProvider, ModelRequest, ModelResult } from './types.js';
4
+ /**
5
+ * 动态模型管理的独立系统能力;配置、列出及连接测试均要求用户持有此能力。
6
+ */
7
+ export declare const MODEL_CAPABILITY = "Sys.ai.models.manage";
8
+ /**
9
+ * 动态模型配置最初版本的历史迁移;密文列不保存 API Key 明文。
10
+ * 结构扩展由后续迁移承担,不修改已发布的名称或 SQL。
11
+ */
12
+ export declare const MODEL_MIGRATION: {
13
+ name: string;
14
+ sql: string;
15
+ };
16
+ /** 系统配置服务:按租户读取,写入与脱敏审计同事务;不进入业务本体和助理工具。 */
17
+ /**
18
+ * 动态模型配置服务,限定应用显式声明的别名和当前租户。
19
+ * 保存通过 advisory lock 与乐观版本防止首次创建及并发覆盖,配置和脱敏审计同事务提交;生成请求使用配置快照,避免地址与密钥混用。
20
+ */
21
+ export declare class ModelManager {
22
+ readonly kernel: Kernel;
23
+ readonly keys: readonly string[];
24
+ /**
25
+ * 绑定内核及应用允许管理的模型 key 列表;构造时不访问数据库或外部网关。
26
+ */
27
+ constructor(kernel: Kernel, keys: readonly string[]);
28
+ /**
29
+ * 要求真实登录用户和模型管理能力;指定 key 时进一步验证其格式与应用声明范围。
30
+ */
31
+ private need;
32
+ /**
33
+ * 拒绝格式非法或应用未声明的模型别名,防止管理接口创建运行时不会使用的任意配置。
34
+ */
35
+ private known;
36
+ /**
37
+ * 读取当前租户配置,并按应用声明 key 返回完整列表;未配置项也以默认 DTO 展示,密文永不返回。
38
+ */
39
+ list(ctx: Ctx): Promise<AiModelConfig[]>;
40
+ /**
41
+ * 验证并规范化服务地址,在租户/别名事务锁下比较版本后保存配置。
42
+ * 首次配置或更换地址必须提供新 API Key,因为密文与地址绑定;不更换凭据时沿用原密文。Responses 压缩只允许对应协议。
43
+ * 版本递增、密文写入与 before/after 脱敏审计共同提交;旧版本返回 CONFLICT,不自动覆盖他人修改。
44
+ */
45
+ save(ctx: Ctx, key: string, raw: unknown): Promise<AiModelConfig>;
46
+ /**
47
+ * 读取当前租户的已声明且已启用模型记录;缺失或停用给出管理配置提示,不静默回退其他网关。
48
+ */
49
+ private configured;
50
+ /**
51
+ * 用单条配置快照创建协议 provider,并把密钥解密延迟到实际请求。
52
+ * 托管模型固定使用 prod 环境及每秒 2 次限额,独立于 ERP 等连接器的 fake 阶段;路径兼容填入已有 Chat/Responses 端点。
53
+ * Responses 接续键绑定租户、别名和配置版本,防止更改地址、模型或凭据后复用旧远端状态。
54
+ */
55
+ private provider;
56
+ /**
57
+ * 读取一次最新配置并返回固定快照 provider;运行时可用同一快照规划预算和执行请求。
58
+ */
59
+ resolve(tenantId: string, key: string): Promise<ModelProvider>;
60
+ /**
61
+ * 每个模型步进解析当前租户配置后执行 generate;已开始的请求继续使用其原快照,不中途切换凭据或地址。
62
+ */
63
+ generate(tenantId: string, key: string, request: ModelRequest): Promise<ModelResult>;
64
+ /**
65
+ * 以固定 Reply OK 提示、无工具和最多 64 输出 Token 探测已保存配置,20 秒中止。
66
+ * 只返回稳定结果、配置版本和延迟毫秒并记录审计;连通成功不证明业务质量、工具调用或完整 Responses 接续能力。
67
+ */
68
+ test(ctx: Ctx, key: string): Promise<AiModelProbe>;
69
+ }
70
+ /** 应用仅声明稳定的模型别名;API、worker 共用数据库中的租户配置。声明时不读取密钥或外呼。 */
71
+ /**
72
+ * 声明一个稳定模型别名,构造阶段不读取密钥、不外呼。
73
+ * 运行时 bind 后用可信 tenantId 解析租户配置;未绑定或缺少可信运行上下文直接失败,不能让模型请求任意指定其他租户。
74
+ */
75
+ export declare function managedModel(key: string): ModelProvider;
package/dist/models.js ADDED
@@ -0,0 +1,241 @@
1
+ /**
2
+ * 管理租户的动态模型配置、加密凭据和连接探测,并绑定 managedModel 声明。
3
+ * Admin 通过独立管理能力修改配置,API/worker 每个模型步进读取一致快照;密钥只在服务端加解密,不进入 DTO、业务本体、助理工具或审计正文。
4
+ */
5
+ import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
6
+ import { cageError } from '@yss123/cage-kernel';
7
+ import { secrets, ConnectorError } from '@yss123/cage-connectors';
8
+ import { modelKeySchema, modelConfigSchema } from '@yss123/cage-ai-protocol';
9
+ import { sql } from 'kysely';
10
+ import { digest, parse } from './security.js';
11
+ import { chatModel } from './model.js';
12
+ import { responsesModel } from './responses.js';
13
+ /**
14
+ * 动态模型管理的独立系统能力;配置、列出及连接测试均要求用户持有此能力。
15
+ */
16
+ export const MODEL_CAPABILITY = 'Sys.ai.models.manage';
17
+ /**
18
+ * 动态模型配置最初版本的历史迁移;密文列不保存 API Key 明文。
19
+ * 结构扩展由后续迁移承担,不修改已发布的名称或 SQL。
20
+ */
21
+ export const MODEL_MIGRATION = { name: 'cage:ai_models_001', sql: `
22
+ CREATE TABLE sys.ai_model (
23
+ tenant_id uuid NOT NULL, key text NOT NULL, version integer NOT NULL,
24
+ label text NOT NULL, base_url text NOT NULL, model text NOT NULL, enabled boolean NOT NULL,
25
+ timeout_ms integer NOT NULL, encrypted_key text NOT NULL, updated_by uuid NOT NULL,
26
+ updated_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY(tenant_id,key)
27
+ );
28
+ GRANT SELECT, INSERT, UPDATE ON sys.ai_model TO cage_app;
29
+ ` };
30
+ /**
31
+ * 从部署 SecretProvider 读取 CAGE_AI_ENCRYPTION_KEY,要求标准 Base64 编码的 32 字节密钥。
32
+ * 缺失或格式错误返回稳定部署提示,不输出密钥内容;每次加解密解析,未在此实现多版本密钥轮换。
33
+ */
34
+ async function encryptionKey() {
35
+ let value;
36
+ try {
37
+ value = await secrets.get('CAGE_AI_ENCRYPTION_KEY');
38
+ }
39
+ catch {
40
+ throw cageError('DOMAIN_ERROR', { message: '部署尚未配置 CAGE_AI_ENCRYPTION_KEY,请配置后再保存模型' });
41
+ }
42
+ const key = Buffer.from(value, 'base64');
43
+ if (key.length !== 32 || key.toString('base64') !== value)
44
+ throw cageError('DOMAIN_ERROR', { message: 'CAGE_AI_ENCRYPTION_KEY 必须是 32 字节随机密钥的标准 Base64 编码' });
45
+ return key;
46
+ }
47
+ /**
48
+ * 把租户、模型别名和服务地址编码成 AES-GCM 附加认证数据,防止密文跨租户、别名或目标地址搬用。
49
+ */
50
+ const aad = (tenantId, key, baseUrl) => Buffer.from(JSON.stringify([tenantId, key, baseUrl]));
51
+ /**
52
+ * 以随机 12 字节 IV 和 AES-256-GCM 加密 API Key,返回含版本、IV、认证标签和密文的封装。
53
+ * 附加认证数据由调用方绑定配置身份;只返回密文,不记录明文或部署密钥。
54
+ */
55
+ async function encrypt(value, associated) {
56
+ const iv = randomBytes(12), cipher = createCipheriv('aes-256-gcm', await encryptionKey(), iv);
57
+ cipher.setAAD(associated);
58
+ const ciphertext = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]);
59
+ return ['v1', iv.toString('base64'), cipher.getAuthTag().toString('base64'), ciphertext.toString('base64')].join(':');
60
+ }
61
+ /**
62
+ * 验证 v1 密文、认证标签和配置绑定信息后恢复请求所需的 API Key。
63
+ * 格式、密钥或绑定不一致统一返回可操作的解密失败提示,避免底层错误暴露密文片段;明文只交给连接器凭据回调。
64
+ */
65
+ async function decrypt(value, associated) {
66
+ const key = await encryptionKey();
67
+ try {
68
+ const [version, iv, tag, ciphertext] = value.split(':');
69
+ if (version !== 'v1')
70
+ throw new Error('unsupported');
71
+ const cipher = createDecipheriv('aes-256-gcm', key, Buffer.from(iv, 'base64'));
72
+ cipher.setAAD(associated);
73
+ cipher.setAuthTag(Buffer.from(tag, 'base64'));
74
+ return Buffer.concat([cipher.update(Buffer.from(ciphertext, 'base64')), cipher.final()]).toString('utf8');
75
+ }
76
+ catch {
77
+ throw cageError('DOMAIN_ERROR', { message: '模型密钥无法解密,请检查部署加密密钥或重新保存 API Key' });
78
+ }
79
+ }
80
+ /**
81
+ * 把数据库模型记录映射为不含密钥的管理 DTO;未配置别名返回版本 0、停用和默认窗口/超时。
82
+ * hasApiKey 仅表示存在密文,不能证明凭据当前可用。
83
+ */
84
+ function dto(key, row) {
85
+ return { key, version: row?.version ?? 0, label: row?.label ?? key, baseUrl: row?.base_url ?? '', model: row?.model ?? '',
86
+ enabled: row?.enabled ?? false, timeoutMs: row?.timeout_ms ?? 90_000, hasApiKey: Boolean(row?.encrypted_key),
87
+ apiFormat: row?.api_format ?? 'chat-completions', contextWindowTokens: row?.context_window_tokens ?? 32768, responsesCompaction: row?.responses_compaction ?? false,
88
+ updatedAt: row ? new Date(row.updated_at).toISOString() : null };
89
+ }
90
+ /** 系统配置服务:按租户读取,写入与脱敏审计同事务;不进入业务本体和助理工具。 */
91
+ /**
92
+ * 动态模型配置服务,限定应用显式声明的别名和当前租户。
93
+ * 保存通过 advisory lock 与乐观版本防止首次创建及并发覆盖,配置和脱敏审计同事务提交;生成请求使用配置快照,避免地址与密钥混用。
94
+ */
95
+ export class ModelManager {
96
+ kernel;
97
+ keys;
98
+ /**
99
+ * 绑定内核及应用允许管理的模型 key 列表;构造时不访问数据库或外部网关。
100
+ */
101
+ constructor(kernel, keys) {
102
+ this.kernel = kernel;
103
+ this.keys = keys;
104
+ }
105
+ /**
106
+ * 要求真实登录用户和模型管理能力;指定 key 时进一步验证其格式与应用声明范围。
107
+ */
108
+ need(ctx, key) {
109
+ if (ctx.actor?.kind !== 'user' || !ctx.userId)
110
+ throw cageError('UNAUTHENTICATED');
111
+ if (!ctx.capabilities.has(MODEL_CAPABILITY))
112
+ throw cageError('FORBIDDEN', { capability: MODEL_CAPABILITY });
113
+ if (key !== undefined)
114
+ this.known(key);
115
+ }
116
+ /**
117
+ * 拒绝格式非法或应用未声明的模型别名,防止管理接口创建运行时不会使用的任意配置。
118
+ */
119
+ known(key) { parse(modelKeySchema, key); if (!this.keys.includes(key))
120
+ throw cageError('NOT_FOUND'); }
121
+ /**
122
+ * 读取当前租户配置,并按应用声明 key 返回完整列表;未配置项也以默认 DTO 展示,密文永不返回。
123
+ */
124
+ async list(ctx) {
125
+ this.need(ctx);
126
+ const rows = await this.kernel.app.selectFrom('sys.ai_model').selectAll().where('tenant_id', '=', ctx.tenantId).execute();
127
+ return this.keys.map(key => dto(key, rows.find(r => r.key === key)));
128
+ }
129
+ /**
130
+ * 验证并规范化服务地址,在租户/别名事务锁下比较版本后保存配置。
131
+ * 首次配置或更换地址必须提供新 API Key,因为密文与地址绑定;不更换凭据时沿用原密文。Responses 压缩只允许对应协议。
132
+ * 版本递增、密文写入与 before/after 脱敏审计共同提交;旧版本返回 CONFLICT,不自动覆盖他人修改。
133
+ */
134
+ async save(ctx, key, raw) {
135
+ this.need(ctx, key);
136
+ const input = parse(modelConfigSchema, raw), baseUrl = new URL(input.baseUrl).toString().replace(/\/$/, '');
137
+ return this.kernel.app.transaction().execute(async (tx) => {
138
+ // 同一别名首次创建也需串行,不能只锁尚不存在的行。
139
+ await sql `select pg_advisory_xact_lock(hashtextextended(${`${ctx.tenantId}:ai-model:${key}`}, 0))`.execute(tx);
140
+ const previous = await tx.selectFrom('sys.ai_model').selectAll().where('tenant_id', '=', ctx.tenantId).where('key', '=', key).forUpdate().executeTakeFirst();
141
+ if ((previous?.version ?? 0) !== input.version)
142
+ throw cageError('CONFLICT');
143
+ const apiFormat = input.apiFormat ?? previous?.api_format ?? 'chat-completions';
144
+ if (input.responsesCompaction && apiFormat !== 'responses')
145
+ throw cageError('INVALID_INPUT', '服务端自动压缩仅适用于 Responses');
146
+ if (!input.apiKey && (!previous || previous.base_url !== baseUrl))
147
+ throw cageError('INVALID_INPUT', { message: '首次配置或更换服务地址时,请同时填写新的 API Key' });
148
+ const encrypted = input.apiKey ? await encrypt(input.apiKey, aad(ctx.tenantId, key, baseUrl)) : previous.encrypted_key;
149
+ const values = { version: input.version + 1, label: input.label, base_url: baseUrl, model: input.model, enabled: input.enabled,
150
+ api_format: apiFormat, context_window_tokens: input.contextWindowTokens ?? previous?.context_window_tokens ?? 32768,
151
+ responses_compaction: apiFormat === 'responses' && (input.responsesCompaction ?? previous?.responses_compaction ?? false),
152
+ timeout_ms: input.timeoutMs, encrypted_key: encrypted, updated_by: ctx.userId, updated_at: new Date() };
153
+ await tx.insertInto('sys.ai_model').values({ tenant_id: ctx.tenantId, key, ...values })
154
+ .onConflict(oc => oc.columns(['tenant_id', 'key']).doUpdateSet(values)).execute();
155
+ const saved = dto(key, values);
156
+ await this.kernel.writeAudit(tx, { tenantId: ctx.tenantId, actor: ctx.actor.id, actorKind: 'user', source: 'api', kind: 'ops',
157
+ capability: MODEL_CAPABILITY, command: 'Sys.ai.models.save', result: 'ok', requestId: ctx.requestId ?? null,
158
+ diff: { before: previous ? dto(key, previous) : null, after: saved, keyChanged: Boolean(input.apiKey) } });
159
+ return saved;
160
+ });
161
+ }
162
+ /**
163
+ * 读取当前租户的已声明且已启用模型记录;缺失或停用给出管理配置提示,不静默回退其他网关。
164
+ */
165
+ async configured(tenantId, key) {
166
+ this.known(key);
167
+ const row = await this.kernel.app.selectFrom('sys.ai_model').selectAll().where('tenant_id', '=', tenantId).where('key', '=', key).executeTakeFirst();
168
+ if (!row?.enabled)
169
+ throw cageError('DOMAIN_ERROR', { message: '模型尚未配置或已停用,请在系统管理中启用' });
170
+ return row;
171
+ }
172
+ /**
173
+ * 用单条配置快照创建协议 provider,并把密钥解密延迟到实际请求。
174
+ * 托管模型固定使用 prod 环境及每秒 2 次限额,独立于 ERP 等连接器的 fake 阶段;路径兼容填入已有 Chat/Responses 端点。
175
+ * Responses 接续键绑定租户、别名和配置版本,防止更改地址、模型或凭据后复用旧远端状态。
176
+ */
177
+ provider(tenantId, key, row) {
178
+ const base = new URL(row.base_url), path = base.pathname.replace(/\/$/, '').replace(/\/(chat\/completions|responses)$/, '');
179
+ const responses = row.api_format === 'responses';
180
+ const provider = (responses ? responsesModel : chatModel)(`managed-${tenantId}-${key}`, { model: row.model, hosts: { prod: base.host }, environment: 'prod',
181
+ protocol: base.protocol,
182
+ path: `${path}/${responses ? 'responses' : 'chat/completions'}`, timeoutMs: row.timeout_ms, rateLimit: { perSecond: 2 },
183
+ contextWindowTokens: row.context_window_tokens, ...{ compaction: row.responses_compaction },
184
+ credential: () => decrypt(row.encrypted_key, aad(tenantId, key, row.base_url)) });
185
+ if (provider.continuation)
186
+ provider.continuation = { ...provider.continuation, key: digest({ tenantId, key, version: row.version }) };
187
+ return provider;
188
+ }
189
+ /**
190
+ * 读取一次最新配置并返回固定快照 provider;运行时可用同一快照规划预算和执行请求。
191
+ */
192
+ async resolve(tenantId, key) {
193
+ return this.provider(tenantId, key, await this.configured(tenantId, key));
194
+ }
195
+ /**
196
+ * 每个模型步进解析当前租户配置后执行 generate;已开始的请求继续使用其原快照,不中途切换凭据或地址。
197
+ */
198
+ async generate(tenantId, key, request) {
199
+ // 每个模型步进读取最新配置;已发出的请求保持原配置快照,避免跨地址串用凭据。
200
+ return (await this.resolve(tenantId, key)).generate(request);
201
+ }
202
+ /**
203
+ * 以固定 Reply OK 提示、无工具和最多 64 输出 Token 探测已保存配置,20 秒中止。
204
+ * 只返回稳定结果、配置版本和延迟毫秒并记录审计;连通成功不证明业务质量、工具调用或完整 Responses 接续能力。
205
+ */
206
+ async test(ctx, key) {
207
+ this.need(ctx, key);
208
+ const row = await this.configured(ctx.tenantId, key), started = Date.now();
209
+ let code = 'CONNECTED';
210
+ try {
211
+ await this.provider(ctx.tenantId, key, row).generate({ messages: [{ role: 'user', content: 'Reply OK.' }], tools: [], maxOutputTokens: 64, signal: AbortSignal.timeout(20_000) });
212
+ }
213
+ catch (error) {
214
+ code = error instanceof ConnectorError && error.status ? `HTTP_${error.status}` : 'MODEL_CONNECTION_FAILED';
215
+ }
216
+ const result = { ok: code === 'CONNECTED', code, version: row.version, latencyMs: Date.now() - started };
217
+ await this.kernel.writeAudit(this.kernel.app, { tenantId: ctx.tenantId, actor: ctx.actor.id, actorKind: 'user', source: 'api', kind: 'ops',
218
+ capability: MODEL_CAPABILITY, command: 'Sys.ai.models.test', result: result.ok ? 'ok' : 'failed', requestId: ctx.requestId ?? null, diff: { key, ...result } });
219
+ return result;
220
+ }
221
+ }
222
+ /** 应用仅声明稳定的模型别名;API、worker 共用数据库中的租户配置。声明时不读取密钥或外呼。 */
223
+ /**
224
+ * 声明一个稳定模型别名,构造阶段不读取密钥、不外呼。
225
+ * 运行时 bind 后用可信 tenantId 解析租户配置;未绑定或缺少可信运行上下文直接失败,不能让模型请求任意指定其他租户。
226
+ */
227
+ export function managedModel(key) {
228
+ parse(modelKeySchema, key);
229
+ const spec = { kind: 'managed-model', key };
230
+ return { spec, async generate() { throw new Error('managedModel 必须由 Cage 运行时绑定'); },
231
+ bind(kernel) {
232
+ const manager = new ModelManager(kernel, [key]);
233
+ return { spec, resolve: context => manager.resolve(context.tenantId, key), generate(request) {
234
+ if (!request.context)
235
+ throw new Error('模型调用缺少可信运行上下文');
236
+ return manager.generate(request.context.tenantId, key, request);
237
+ } };
238
+ },
239
+ };
240
+ }
241
+ //# sourceMappingURL=models.js.map