@ct-agents/worker 0.4.1 → 0.5.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ct-agents/worker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src"
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"zod": "4.4.3",
|
|
15
|
-
"@ct-agents/
|
|
16
|
-
"@ct-agents/
|
|
17
|
-
"@ct-agents/
|
|
18
|
-
"@ct-agents/
|
|
19
|
-
"@ct-agents/
|
|
20
|
-
"@ct-agents/
|
|
15
|
+
"@ct-agents/environment": "0.5.0",
|
|
16
|
+
"@ct-agents/memory": "0.5.0",
|
|
17
|
+
"@ct-agents/harness": "0.5.0",
|
|
18
|
+
"@ct-agents/prompts": "0.5.0",
|
|
19
|
+
"@ct-agents/protocol": "0.5.0",
|
|
20
|
+
"@ct-agents/tools": "0.5.0"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
23
|
"dockerode": "4.0.9"
|
package/src/index.ts
CHANGED
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
createWebSearchToolHandler,
|
|
35
35
|
} from '@ct-agents/tools/builtin-tools';
|
|
36
36
|
import { z } from 'zod';
|
|
37
|
-
import { createPlatformResourceProxy } from './resources/platform-proxy.js';
|
|
37
|
+
import { createPlatformResourceProxy, createPlatformTextGenerationProxy } from './resources/platform-proxy.js';
|
|
38
38
|
import { WorkerHttpTransport } from './worker-http-transport.js';
|
|
39
39
|
import { runLeasedItem } from './leased-item-runner.js';
|
|
40
40
|
|
|
@@ -86,6 +86,8 @@ export type WorkerSandboxManager = {
|
|
|
86
86
|
};
|
|
87
87
|
|
|
88
88
|
export type EnvironmentWorkerInput = {
|
|
89
|
+
/** 当前注册实例身份,仅用于绑定平台代理的 lease audit。 */
|
|
90
|
+
workerInstanceId?: string;
|
|
89
91
|
workQueue: WorkerWorkQueue;
|
|
90
92
|
toolRegistry: ToolRegistry;
|
|
91
93
|
resources?: ResourceSlots;
|
|
@@ -125,6 +127,7 @@ export const DEFAULT_CLAIM_BLOCK_MS = 15_000;
|
|
|
125
127
|
const PLATFORM_PROXY_RESOURCE_CAPABILITIES = [
|
|
126
128
|
{ slot: 'skills', implementationId: 'platform-skills-store' },
|
|
127
129
|
{ slot: 'memory', implementationId: 'platform-memory-store' },
|
|
130
|
+
{ slot: 'textGeneration', implementationId: 'platform-model' },
|
|
128
131
|
] as const;
|
|
129
132
|
const PLATFORM_PROXY_RESOURCE_SLOTS = new Set<string>(
|
|
130
133
|
PLATFORM_PROXY_RESOURCE_CAPABILITIES.map((capability) => capability.slot),
|
|
@@ -171,7 +174,8 @@ export function createWorkerCapabilityRegistration(input: {
|
|
|
171
174
|
};
|
|
172
175
|
});
|
|
173
176
|
const resourceCapabilities = Object.entries(input.resourceImpls).flatMap(([slot, implementations]) => (
|
|
174
|
-
Object.keys(implementations).
|
|
177
|
+
Object.keys(implementations).filter((implementationId) => !(slot === 'textGeneration' && implementationId === 'platform-model'))
|
|
178
|
+
.map((implementationId) => ({ slot, implementationId }))
|
|
175
179
|
));
|
|
176
180
|
if (input.platformResourceProxy) {
|
|
177
181
|
for (const capability of PLATFORM_PROXY_RESOURCE_CAPABILITIES) {
|
|
@@ -388,6 +392,18 @@ export class EnvironmentWorker {
|
|
|
388
392
|
const resources: ResourceSlots = {};
|
|
389
393
|
for (const [slotName, bindingValue] of Object.entries(item.resourceBindings)) {
|
|
390
394
|
const binding = parseResourceBinding(slotName, bindingValue);
|
|
395
|
+
if (slotName === 'textGeneration' && binding.implementationId === 'platform-model') {
|
|
396
|
+
const proxy = this.input.platformResourceProxy;
|
|
397
|
+
if (!proxy || !item.leaseId || !this.input.workerInstanceId?.trim()) throw new Error('平台文本生成缺少代理配置或当前 lease/Worker 身份');
|
|
398
|
+
const timeoutMs = binding.options.timeoutMs;
|
|
399
|
+
if (typeof timeoutMs !== 'number' || !Number.isInteger(timeoutMs) || timeoutMs < 1000 || timeoutMs > 120000) throw new Error('平台文本生成 binding timeoutMs 无效');
|
|
400
|
+
const signals = [proxy.signal, this.input.abortSignal].filter((signal): signal is AbortSignal => Boolean(signal));
|
|
401
|
+
resources[slotName] = createPlatformTextGenerationProxy({ ...proxy, environmentId: item.environmentId, timeoutMs,
|
|
402
|
+
signal: signals.length ? AbortSignal.any(signals) : undefined,
|
|
403
|
+
audit: { workId: item.workId, sessionId: item.sessionId, toolCallEventId: item.toolCallEventId,
|
|
404
|
+
leaseId: item.leaseId, workerInstanceId: this.input.workerInstanceId } });
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
391
407
|
const implementations = this.input.resourceImpls?.[slotName];
|
|
392
408
|
const implementation = implementations?.[binding.implementationId];
|
|
393
409
|
if (!implementation) {
|
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import { platformApiPath } from '../platform-api-path.js';
|
|
2
|
+
import { MODEL_TIERS, type TextGenerationResource } from '@ct-agents/protocol';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
const textGenerationResultBase = z.object({
|
|
6
|
+
modelTier: z.enum(MODEL_TIERS), provider: z.enum(['openai', 'anthropic']),
|
|
7
|
+
model: z.string().min(1), durationMs: z.number().nonnegative(),
|
|
8
|
+
});
|
|
9
|
+
const textGenerationResultSchema = z.discriminatedUnion('success', [
|
|
10
|
+
textGenerationResultBase.extend({ success: z.literal(true), text: z.string() }),
|
|
11
|
+
textGenerationResultBase.extend({ success: z.literal(false), error: z.object({
|
|
12
|
+
code: z.enum(['TEXT_GENERATION_INVALID_REQUEST', 'TEXT_GENERATION_AUTH_FAILED', 'TEXT_GENERATION_RATE_LIMITED',
|
|
13
|
+
'TEXT_GENERATION_TIMEOUT', 'TEXT_GENERATION_EMPTY_RESPONSE', 'TEXT_GENERATION_PROVIDER_UNAVAILABLE', 'TEXT_GENERATION_PROVIDER_ERROR']),
|
|
14
|
+
message: z.string(), retryable: z.boolean(),
|
|
15
|
+
}) }),
|
|
16
|
+
]);
|
|
2
17
|
|
|
3
18
|
export type PlatformResourceProxyInput = {
|
|
4
19
|
baseUrl: string;
|
|
@@ -70,6 +85,8 @@ async function parseProxyEnvelope(response: Response): Promise<unknown> {
|
|
|
70
85
|
* worker 只持有 environment key,不持有平台内置 resource 的原始密钥。
|
|
71
86
|
*/
|
|
72
87
|
export async function invokePlatformResourceProxy(input: InvokePlatformResourceProxyInput): Promise<unknown> {
|
|
88
|
+
const textGeneration = input.slot === 'textGeneration' && input.implementationId === 'platform-model' && input.method === 'generate';
|
|
89
|
+
if (textGeneration && input.signal?.aborted) throw new DOMException('请求已取消', 'AbortError');
|
|
73
90
|
const baseUrl = trimTrailingSlash(input.baseUrl);
|
|
74
91
|
const fetchImpl = input.fetchImpl ?? fetch;
|
|
75
92
|
const headers = new Headers();
|
|
@@ -99,6 +116,7 @@ export async function invokePlatformResourceProxy(input: InvokePlatformResourceP
|
|
|
99
116
|
});
|
|
100
117
|
return await parseProxyEnvelope(response);
|
|
101
118
|
} catch (error) {
|
|
119
|
+
if (textGeneration && input.signal?.aborted) throw new DOMException('请求已取消', 'AbortError');
|
|
102
120
|
if (isAbortError(error)) {
|
|
103
121
|
throw new Error('平台 resource proxy 请求超时', { cause: error });
|
|
104
122
|
}
|
|
@@ -109,6 +127,27 @@ export async function invokePlatformResourceProxy(input: InvokePlatformResourceP
|
|
|
109
127
|
}
|
|
110
128
|
}
|
|
111
129
|
|
|
130
|
+
/** 文本生成的 AbortSignal 留在本地传输层,身份来自领取上下文,不进入业务参数。 */
|
|
131
|
+
export function createPlatformTextGenerationProxy(input: Omit<PlatformResourceProxyInput, 'slot' | 'implementationId' | 'audit' | 'timeoutMs'> & {
|
|
132
|
+
timeoutMs: number;
|
|
133
|
+
audit: PlatformResourceProxyInput['audit'] & { leaseId: string; workerInstanceId: string };
|
|
134
|
+
}): TextGenerationResource {
|
|
135
|
+
return {
|
|
136
|
+
async generate(request, context) {
|
|
137
|
+
const signals = [input.signal, context?.abortSignal].filter((signal): signal is AbortSignal => Boolean(signal));
|
|
138
|
+
const signal = signals.length ? AbortSignal.any(signals) : undefined;
|
|
139
|
+
const result = await invokePlatformResourceProxy({ ...input, slot: 'textGeneration', implementationId: 'platform-model',
|
|
140
|
+
method: 'generate', args: [request], timeoutMs: input.timeoutMs + 5000,
|
|
141
|
+
requestTimeoutMs: Math.min(input.requestTimeoutMs ?? Infinity, input.timeoutMs + 10000), signal });
|
|
142
|
+
const parsed = textGenerationResultSchema.safeParse(result);
|
|
143
|
+
if (!parsed.success) {
|
|
144
|
+
throw new Error('平台文本生成响应不符合协议');
|
|
145
|
+
}
|
|
146
|
+
return parsed.data;
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
112
151
|
/**
|
|
113
152
|
* 创建一个通过平台 Resource proxy 访问的动态 resource。
|
|
114
153
|
*
|
|
@@ -1,402 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
TextGenerationResult,
|
|
9
|
-
} from '@ct-agents/protocol';
|
|
10
|
-
import { ModelOutputError, ModelProviderError } from '@ct-agents/harness';
|
|
11
|
-
import { runAnthropic } from '@ct-agents/harness/llm/anthropic-provider.js';
|
|
12
|
-
import type {
|
|
13
|
-
NormalizedModelOutput,
|
|
14
|
-
ResolvedProviderConfig,
|
|
15
|
-
} from '@ct-agents/harness/llm/model-port.js';
|
|
16
|
-
import { runOpenAI } from '@ct-agents/harness/llm/openai-provider.js';
|
|
17
|
-
import { z } from 'zod';
|
|
18
|
-
import type { EnvironmentWorkerResourceImplementation } from '../../index.js';
|
|
19
|
-
|
|
20
|
-
const DEFAULT_MAX_OUTPUT_TOKENS = 4_096;
|
|
21
|
-
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
22
|
-
const DEFAULT_PROVIDER = 'anthropic' as const;
|
|
23
|
-
const DEFAULT_MODEL = 'deepseek-v4-flash';
|
|
24
|
-
const MAX_PROMPT_BYTES = 512 * 1_024;
|
|
25
|
-
const MAX_SYSTEM_PROMPT_BYTES = 32 * 1_024;
|
|
26
|
-
const MAX_RESPONSE_BYTES = 1_024 * 1_024;
|
|
27
|
-
|
|
28
|
-
export type TextGenerationOptions = {
|
|
29
|
-
provider: TextGenerationProvider;
|
|
30
|
-
apiKey: string;
|
|
31
|
-
defaultModel: string;
|
|
32
|
-
baseURL?: string;
|
|
33
|
-
maxOutputTokens: number;
|
|
34
|
-
timeoutMs: number;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const providerBaseUrlSchema = z.string().trim().max(2_048).url().refine((value) => {
|
|
38
|
-
const url = new URL(value);
|
|
39
|
-
return (url.protocol === 'https:' || url.protocol === 'http:')
|
|
40
|
-
&& !url.username
|
|
41
|
-
&& !url.password;
|
|
42
|
-
}, 'baseURL 必须是无用户凭证的 HTTP(S) URL');
|
|
43
|
-
|
|
44
|
-
export const textGenerationOptionsSchema = z.object({
|
|
45
|
-
provider: z.enum(['openai', 'anthropic']),
|
|
46
|
-
apiKey: z.string().trim().min(1).meta({ secret: true }),
|
|
47
|
-
defaultModel: z.string().trim().min(1).max(256),
|
|
48
|
-
baseURL: providerBaseUrlSchema.optional(),
|
|
49
|
-
maxOutputTokens: z.number().int().min(128).max(16_384).default(DEFAULT_MAX_OUTPUT_TOKENS),
|
|
50
|
-
timeoutMs: z.number().int().min(1_000).max(120_000).default(DEFAULT_TIMEOUT_MS),
|
|
51
|
-
}).strict();
|
|
52
|
-
|
|
53
|
-
const textGenerationInputSchema = z.object({
|
|
54
|
-
prompt: z.string().min(1),
|
|
55
|
-
systemPrompt: z.string().optional(),
|
|
56
|
-
model: z.string().trim().min(1).max(256).optional(),
|
|
57
|
-
maxOutputTokens: z.number().int().min(128).max(16_384).optional(),
|
|
58
|
-
}).strict();
|
|
59
|
-
|
|
60
|
-
export type TextGenerationProviderRunInput = {
|
|
61
|
-
provider: TextGenerationProvider;
|
|
62
|
-
config: ResolvedProviderConfig;
|
|
63
|
-
input: HarnessModelRunInput;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
export type TextGenerationResourceDependencies = {
|
|
67
|
-
runProvider?: (input: TextGenerationProviderRunInput) => Promise<NormalizedModelOutput>;
|
|
68
|
-
now?: () => number;
|
|
69
|
-
defaultAbortSignal?: AbortSignal;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
function byteLength(value: string): number {
|
|
73
|
-
return new TextEncoder().encode(value).byteLength;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function durationSince(startedAt: number, now: () => number): number {
|
|
77
|
-
return Math.max(0, Math.round(now() - startedAt));
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function failure(input: {
|
|
81
|
-
provider: TextGenerationProvider;
|
|
82
|
-
model: string;
|
|
83
|
-
startedAt: number;
|
|
84
|
-
now: () => number;
|
|
85
|
-
code: TextGenerationErrorCode;
|
|
86
|
-
message: string;
|
|
87
|
-
retryable: boolean;
|
|
88
|
-
}): TextGenerationResult {
|
|
89
|
-
return {
|
|
90
|
-
success: false,
|
|
91
|
-
provider: input.provider,
|
|
92
|
-
model: input.model,
|
|
93
|
-
durationMs: durationSince(input.startedAt, input.now),
|
|
94
|
-
error: {
|
|
95
|
-
code: input.code,
|
|
96
|
-
message: input.message,
|
|
97
|
-
retryable: input.retryable,
|
|
98
|
-
},
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function readHttpStatus(error: unknown): number | undefined {
|
|
103
|
-
const visited = new Set<unknown>();
|
|
104
|
-
let current: unknown = error;
|
|
105
|
-
while (current && typeof current === 'object' && !visited.has(current)) {
|
|
106
|
-
visited.add(current);
|
|
107
|
-
const status = 'status' in current ? (current as { status?: unknown }).status : undefined;
|
|
108
|
-
if (typeof status === 'number' && Number.isFinite(status)) {
|
|
109
|
-
return status;
|
|
110
|
-
}
|
|
111
|
-
current = 'cause' in current ? (current as { cause?: unknown }).cause : undefined;
|
|
112
|
-
}
|
|
113
|
-
return undefined;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function classifyProviderError(error: unknown): {
|
|
117
|
-
code: TextGenerationErrorCode;
|
|
118
|
-
message: string;
|
|
119
|
-
retryable: boolean;
|
|
120
|
-
} {
|
|
121
|
-
if (error instanceof ModelOutputError) {
|
|
122
|
-
const isCompletedEmpty = error.diagnostic.stage === 'response_completion'
|
|
123
|
-
&& [
|
|
124
|
-
'completed_empty',
|
|
125
|
-
'end_turn',
|
|
126
|
-
'stop_sequence',
|
|
127
|
-
'tool_use',
|
|
128
|
-
'refusal',
|
|
129
|
-
'missing',
|
|
130
|
-
].includes(error.diagnostic.completionReason);
|
|
131
|
-
return isCompletedEmpty
|
|
132
|
-
? {
|
|
133
|
-
code: 'TEXT_GENERATION_EMPTY_RESPONSE',
|
|
134
|
-
message: '模型服务返回了空文本',
|
|
135
|
-
retryable: false,
|
|
136
|
-
}
|
|
137
|
-
: {
|
|
138
|
-
code: 'TEXT_GENERATION_PROVIDER_ERROR',
|
|
139
|
-
message: '模型服务返回了非预期错误',
|
|
140
|
-
retryable: true,
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
const status = readHttpStatus(error);
|
|
144
|
-
if (status === 400 || status === 404 || status === 422) {
|
|
145
|
-
return {
|
|
146
|
-
code: 'TEXT_GENERATION_INVALID_REQUEST',
|
|
147
|
-
message: '模型服务拒绝了生成请求,请检查 Environment 模型配置',
|
|
148
|
-
retryable: false,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
if (status === 401 || status === 403) {
|
|
152
|
-
return {
|
|
153
|
-
code: 'TEXT_GENERATION_AUTH_FAILED',
|
|
154
|
-
message: '模型服务认证失败,请检查 Environment 密钥',
|
|
155
|
-
retryable: false,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
if (status === 429) {
|
|
159
|
-
return {
|
|
160
|
-
code: 'TEXT_GENERATION_RATE_LIMITED',
|
|
161
|
-
message: '模型服务请求过于频繁,请稍后重试',
|
|
162
|
-
retryable: true,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
if (status !== undefined && status >= 500) {
|
|
166
|
-
return {
|
|
167
|
-
code: 'TEXT_GENERATION_PROVIDER_UNAVAILABLE',
|
|
168
|
-
message: '模型服务暂时不可用,请稍后重试',
|
|
169
|
-
retryable: true,
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
return {
|
|
173
|
-
code: 'TEXT_GENERATION_PROVIDER_ERROR',
|
|
174
|
-
message: '模型服务返回了非预期错误',
|
|
175
|
-
retryable: status === undefined && !(error instanceof ModelProviderError),
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function abortError(reason?: unknown): Error {
|
|
180
|
-
if (reason instanceof Error && reason.name === 'AbortError') {
|
|
181
|
-
return reason;
|
|
182
|
-
}
|
|
183
|
-
return new DOMException('aborted', 'AbortError');
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
async function defaultRunProvider(
|
|
187
|
-
input: TextGenerationProviderRunInput,
|
|
188
|
-
): Promise<NormalizedModelOutput> {
|
|
189
|
-
return input.provider === 'openai'
|
|
190
|
-
? runOpenAI(input.input, input.config)
|
|
191
|
-
: runAnthropic(input.input, input.config);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function buildModelInput(input: TextGenerationInput, abortSignal: AbortSignal): HarnessModelRunInput {
|
|
195
|
-
const systemPrompt = input.systemPrompt?.trim() ?? '';
|
|
196
|
-
return {
|
|
197
|
-
context: {
|
|
198
|
-
messages: [
|
|
199
|
-
...(systemPrompt
|
|
200
|
-
? [{
|
|
201
|
-
role: 'system' as const,
|
|
202
|
-
content: [{ type: 'text' as const, text: systemPrompt }],
|
|
203
|
-
}]
|
|
204
|
-
: []),
|
|
205
|
-
{
|
|
206
|
-
role: 'user',
|
|
207
|
-
content: [{ type: 'text', text: input.prompt }],
|
|
208
|
-
},
|
|
209
|
-
],
|
|
210
|
-
},
|
|
211
|
-
systemPrompt,
|
|
212
|
-
tools: [],
|
|
213
|
-
abortSignal,
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* 创建不进入 Harness Agent loop 的单轮文本生成 Resource。
|
|
219
|
-
*
|
|
220
|
-
* Environment 冻结 provider、凭证、baseURL 和最大预算;调用方只能在该边界内选择模型和更小预算。
|
|
221
|
-
*/
|
|
222
|
-
export function createTextGenerationResource(
|
|
223
|
-
rawOptions: TextGenerationOptions,
|
|
224
|
-
dependencies: TextGenerationResourceDependencies = {},
|
|
225
|
-
): TextGenerationResource {
|
|
226
|
-
const options = textGenerationOptionsSchema.parse(rawOptions);
|
|
227
|
-
const runProvider = dependencies.runProvider ?? defaultRunProvider;
|
|
228
|
-
const now = dependencies.now ?? Date.now;
|
|
229
|
-
|
|
230
|
-
return {
|
|
231
|
-
async generate(rawInput, context) {
|
|
232
|
-
const startedAt = now();
|
|
233
|
-
const candidateModel = typeof rawInput?.model === 'string' && rawInput.model.trim()
|
|
234
|
-
? rawInput.model.trim()
|
|
235
|
-
: options.defaultModel;
|
|
236
|
-
const parsedInput = textGenerationInputSchema.safeParse(rawInput);
|
|
237
|
-
if (!parsedInput.success) {
|
|
238
|
-
return failure({
|
|
239
|
-
provider: options.provider,
|
|
240
|
-
model: candidateModel,
|
|
241
|
-
startedAt,
|
|
242
|
-
now,
|
|
243
|
-
code: 'TEXT_GENERATION_INVALID_REQUEST',
|
|
244
|
-
message: '文本生成请求不合法',
|
|
245
|
-
retryable: false,
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
const input = parsedInput.data;
|
|
249
|
-
const model = input.model ?? options.defaultModel;
|
|
250
|
-
if (
|
|
251
|
-
byteLength(input.prompt) > MAX_PROMPT_BYTES
|
|
252
|
-
|| byteLength(input.systemPrompt ?? '') > MAX_SYSTEM_PROMPT_BYTES
|
|
253
|
-
) {
|
|
254
|
-
return failure({
|
|
255
|
-
provider: options.provider,
|
|
256
|
-
model,
|
|
257
|
-
startedAt,
|
|
258
|
-
now,
|
|
259
|
-
code: 'TEXT_GENERATION_INVALID_REQUEST',
|
|
260
|
-
message: '文本生成输入超过 Resource 安全上限',
|
|
261
|
-
retryable: false,
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
const maxOutputTokens = input.maxOutputTokens ?? options.maxOutputTokens;
|
|
265
|
-
if (maxOutputTokens > options.maxOutputTokens) {
|
|
266
|
-
return failure({
|
|
267
|
-
provider: options.provider,
|
|
268
|
-
model,
|
|
269
|
-
startedAt,
|
|
270
|
-
now,
|
|
271
|
-
code: 'TEXT_GENERATION_INVALID_REQUEST',
|
|
272
|
-
message: '调用级输出预算不能超过 Environment 上限',
|
|
273
|
-
retryable: false,
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const upstreamSignal = context?.abortSignal ?? dependencies.defaultAbortSignal;
|
|
278
|
-
if (upstreamSignal?.aborted) {
|
|
279
|
-
throw abortError(upstreamSignal.reason);
|
|
280
|
-
}
|
|
281
|
-
const controller = new AbortController();
|
|
282
|
-
let timedOut = false;
|
|
283
|
-
const onUpstreamAbort = () => controller.abort(upstreamSignal?.reason);
|
|
284
|
-
upstreamSignal?.addEventListener('abort', onUpstreamAbort, { once: true });
|
|
285
|
-
const timer = setTimeout(() => {
|
|
286
|
-
timedOut = true;
|
|
287
|
-
controller.abort();
|
|
288
|
-
}, options.timeoutMs);
|
|
289
|
-
|
|
290
|
-
try {
|
|
291
|
-
const output = await runProvider({
|
|
292
|
-
provider: options.provider,
|
|
293
|
-
config: {
|
|
294
|
-
model,
|
|
295
|
-
apiKey: options.apiKey,
|
|
296
|
-
...(options.baseURL ? { baseURL: options.baseURL } : {}),
|
|
297
|
-
maxOutputTokens,
|
|
298
|
-
},
|
|
299
|
-
input: buildModelInput(input, controller.signal),
|
|
300
|
-
});
|
|
301
|
-
const text = output.text.trim();
|
|
302
|
-
if (!text) {
|
|
303
|
-
return failure({
|
|
304
|
-
provider: options.provider,
|
|
305
|
-
model,
|
|
306
|
-
startedAt,
|
|
307
|
-
now,
|
|
308
|
-
code: 'TEXT_GENERATION_EMPTY_RESPONSE',
|
|
309
|
-
message: '模型服务返回了空文本',
|
|
310
|
-
retryable: false,
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
if (output.toolCalls.length > 0 || byteLength(text) > MAX_RESPONSE_BYTES) {
|
|
314
|
-
return failure({
|
|
315
|
-
provider: options.provider,
|
|
316
|
-
model,
|
|
317
|
-
startedAt,
|
|
318
|
-
now,
|
|
319
|
-
code: 'TEXT_GENERATION_PROVIDER_ERROR',
|
|
320
|
-
message: '模型服务返回了不符合单轮文本契约的结果',
|
|
321
|
-
retryable: false,
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
return {
|
|
325
|
-
success: true,
|
|
326
|
-
provider: options.provider,
|
|
327
|
-
model,
|
|
328
|
-
text,
|
|
329
|
-
durationMs: durationSince(startedAt, now),
|
|
330
|
-
};
|
|
331
|
-
} catch (error) {
|
|
332
|
-
if (upstreamSignal?.aborted) {
|
|
333
|
-
throw abortError(upstreamSignal.reason ?? error);
|
|
334
|
-
}
|
|
335
|
-
if (timedOut) {
|
|
336
|
-
return failure({
|
|
337
|
-
provider: options.provider,
|
|
338
|
-
model,
|
|
339
|
-
startedAt,
|
|
340
|
-
now,
|
|
341
|
-
code: 'TEXT_GENERATION_TIMEOUT',
|
|
342
|
-
message: '文本生成超时,请稍后重试',
|
|
343
|
-
retryable: true,
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
const classified = classifyProviderError(error);
|
|
347
|
-
return failure({
|
|
348
|
-
provider: options.provider,
|
|
349
|
-
model,
|
|
350
|
-
startedAt,
|
|
351
|
-
now,
|
|
352
|
-
...classified,
|
|
353
|
-
});
|
|
354
|
-
} finally {
|
|
355
|
-
clearTimeout(timer);
|
|
356
|
-
upstreamSignal?.removeEventListener('abort', onUpstreamAbort);
|
|
357
|
-
}
|
|
358
|
-
},
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
export function createTextGenerationResourceImplementation(
|
|
363
|
-
dependencies: TextGenerationResourceDependencies = {},
|
|
364
|
-
): EnvironmentWorkerResourceImplementation<TextGenerationResource, TextGenerationOptions> {
|
|
365
|
-
return {
|
|
366
|
-
title: '模型文本生成',
|
|
367
|
-
description: '使用 Environment 配置的模型服务执行单轮、无 Tool 的文本生成。',
|
|
368
|
-
options: textGenerationOptionsSchema,
|
|
369
|
-
factory: (options, context) => createTextGenerationResource(options, {
|
|
370
|
-
...dependencies,
|
|
371
|
-
defaultAbortSignal: context.abortSignal ?? dependencies.defaultAbortSignal,
|
|
372
|
-
}),
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
export function createTextGenerationResourceDefinition(
|
|
377
|
-
dependencies: TextGenerationResourceDependencies = {},
|
|
378
|
-
): ResourceDefinition<TextGenerationResource> {
|
|
379
|
-
return {
|
|
380
|
-
id: 'textGeneration',
|
|
381
|
-
title: '模型文本生成',
|
|
382
|
-
description: '供应商无关的单轮文本生成 Resource。',
|
|
383
|
-
implementations: [{
|
|
384
|
-
id: 'provider-api',
|
|
385
|
-
title: '模型 Provider API',
|
|
386
|
-
description: '通过 OpenAI 或 Anthropic 兼容 API 生成文本。',
|
|
387
|
-
supportedExecutionModes: ['hosted', 'self_hosted'],
|
|
388
|
-
defaultOptions: {
|
|
389
|
-
provider: DEFAULT_PROVIDER,
|
|
390
|
-
defaultModel: DEFAULT_MODEL,
|
|
391
|
-
maxOutputTokens: DEFAULT_MAX_OUTPUT_TOKENS,
|
|
392
|
-
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
393
|
-
},
|
|
394
|
-
optionsSchema: textGenerationOptionsSchema,
|
|
395
|
-
optionsJsonSchema: z.toJSONSchema(textGenerationOptionsSchema),
|
|
396
|
-
factory: (options) => createTextGenerationResource(
|
|
397
|
-
textGenerationOptionsSchema.parse(options),
|
|
398
|
-
dependencies,
|
|
399
|
-
),
|
|
400
|
-
}],
|
|
401
|
-
};
|
|
402
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
platformModelOptionsSchema as textGenerationOptionsSchema,
|
|
3
|
+
createPlatformModelResource as createTextGenerationResource,
|
|
4
|
+
createPlatformModelResourceDefinition as createTextGenerationResourceDefinition,
|
|
5
|
+
} from './platform-model.js';
|
|
6
|
+
export type { TextGenerationProviderRunInput } from './provider-executor.js';
|
|
7
|
+
export type { TextGenerationOptions } from './platform-model.js';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { MODEL_TIERS, MODEL_TIER_TITLES, type ResourceDefinition, type TextGenerationResource } from '@ct-agents/protocol';
|
|
2
|
+
import type { ModelTierRegistry } from '@ct-agents/harness';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { createProviderTextGenerationResource, type TextGenerationResourceDependencies } from './provider-executor.js';
|
|
5
|
+
|
|
6
|
+
export const platformModelOptionsSchema = z.object({
|
|
7
|
+
defaultModel: z.enum(MODEL_TIERS).default('balanced').meta({ title: '默认档位' }),
|
|
8
|
+
maxOutputTokens: z.number().int().min(128).max(16384).default(4096).meta({ title: '输出上限' }),
|
|
9
|
+
timeoutMs: z.number().int().min(1000).max(120000).default(60000).meta({ title: '超时(毫秒)' }),
|
|
10
|
+
}).strict();
|
|
11
|
+
|
|
12
|
+
export type TextGenerationOptions = z.output<typeof platformModelOptionsSchema>;
|
|
13
|
+
|
|
14
|
+
const inputSchema = z.object({
|
|
15
|
+
prompt: z.string().min(1), systemPrompt: z.string().optional(),
|
|
16
|
+
model: z.enum(MODEL_TIERS).optional(), maxOutputTokens: z.number().int().min(128).max(16384).optional(),
|
|
17
|
+
}).strict();
|
|
18
|
+
|
|
19
|
+
/** 平台工厂持有部署快照,公开请求只能选择档位和收紧预算。 */
|
|
20
|
+
export function createPlatformModelResource(
|
|
21
|
+
rawOptions: z.input<typeof platformModelOptionsSchema>,
|
|
22
|
+
dependencies: TextGenerationResourceDependencies & { modelRegistry: ModelTierRegistry },
|
|
23
|
+
): TextGenerationResource {
|
|
24
|
+
const options = platformModelOptionsSchema.parse(rawOptions);
|
|
25
|
+
return {
|
|
26
|
+
async generate(rawInput, context) {
|
|
27
|
+
const parsed = inputSchema.safeParse(rawInput);
|
|
28
|
+
const modelTier = parsed.success ? parsed.data.model ?? options.defaultModel : options.defaultModel;
|
|
29
|
+
const target = dependencies.modelRegistry.resolve(modelTier);
|
|
30
|
+
if (!parsed.success) return { success: false, modelTier, provider: target.provider, model: target.model, durationMs: 0,
|
|
31
|
+
error: { code: 'TEXT_GENERATION_INVALID_REQUEST', message: '文本生成请求不合法', retryable: false } };
|
|
32
|
+
const resource = createProviderTextGenerationResource({ ...options, provider: target.provider, apiKey: target.apiKey,
|
|
33
|
+
baseURL: target.baseURL ?? (target.provider === 'openai' ? 'https://api.openai.com/v1' : 'https://api.anthropic.com'), defaultModel: target.model }, dependencies);
|
|
34
|
+
const result = await resource.generate({ ...parsed.data, model: target.model }, context);
|
|
35
|
+
return { ...result, modelTier };
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createPlatformModelResourceDefinition(
|
|
41
|
+
dependencies: TextGenerationResourceDependencies & { modelRegistry: ModelTierRegistry },
|
|
42
|
+
): ResourceDefinition<TextGenerationResource> {
|
|
43
|
+
const jsonSchema = z.toJSONSchema(platformModelOptionsSchema);
|
|
44
|
+
const field = jsonSchema.properties?.defaultModel;
|
|
45
|
+
if (field && typeof field === 'object') {
|
|
46
|
+
field.oneOf = MODEL_TIERS.map((tier) => ({ const: tier, title: MODEL_TIER_TITLES[tier] }));
|
|
47
|
+
}
|
|
48
|
+
return { id: 'textGeneration', title: '模型文本生成', description: '平台部署映射驱动的单轮文本生成。', implementations: [{
|
|
49
|
+
id: 'platform-model', title: '平台模型服务', supportedExecutionModes: ['hosted', 'self_hosted'],
|
|
50
|
+
defaultOptions: platformModelOptionsSchema.parse({}), optionsSchema: platformModelOptionsSchema, optionsJsonSchema: jsonSchema,
|
|
51
|
+
factory: (options) => createPlatformModelResource(platformModelOptionsSchema.parse(options), dependencies),
|
|
52
|
+
}] };
|
|
53
|
+
}
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HarnessModelRunInput,
|
|
3
|
+
TextGenerationErrorCode,
|
|
4
|
+
TextGenerationInput,
|
|
5
|
+
TextGenerationProvider,
|
|
6
|
+
TextGenerationRequestContext,
|
|
7
|
+
TextGenerationResult,
|
|
8
|
+
} from '@ct-agents/protocol';
|
|
9
|
+
import { ModelOutputError, ModelProviderError } from '@ct-agents/harness';
|
|
10
|
+
import { runAnthropic } from '@ct-agents/harness/llm/anthropic-provider.js';
|
|
11
|
+
import type {
|
|
12
|
+
NormalizedModelOutput,
|
|
13
|
+
ResolvedProviderConfig,
|
|
14
|
+
} from '@ct-agents/harness/llm/model-port.js';
|
|
15
|
+
import { runOpenAI } from '@ct-agents/harness/llm/openai-provider.js';
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
const DEFAULT_MAX_OUTPUT_TOKENS = 4_096;
|
|
20
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
const MAX_PROMPT_BYTES = 512 * 1_024;
|
|
24
|
+
const MAX_SYSTEM_PROMPT_BYTES = 32 * 1_024;
|
|
25
|
+
const MAX_RESPONSE_BYTES = 1_024 * 1_024;
|
|
26
|
+
|
|
27
|
+
export type TextGenerationOptions = {
|
|
28
|
+
provider: TextGenerationProvider;
|
|
29
|
+
apiKey: string;
|
|
30
|
+
defaultModel: string;
|
|
31
|
+
baseURL?: string;
|
|
32
|
+
maxOutputTokens: number;
|
|
33
|
+
timeoutMs: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const providerBaseUrlSchema = z.string().trim().max(2_048).url().refine((value) => {
|
|
37
|
+
const url = new URL(value);
|
|
38
|
+
return (url.protocol === 'https:' || url.protocol === 'http:')
|
|
39
|
+
&& !url.username
|
|
40
|
+
&& !url.password;
|
|
41
|
+
}, 'baseURL 必须是无用户凭证的 HTTP(S) URL');
|
|
42
|
+
|
|
43
|
+
export const textGenerationOptionsSchema = z.object({
|
|
44
|
+
provider: z.enum(['openai', 'anthropic']),
|
|
45
|
+
apiKey: z.string().trim().min(1).meta({ secret: true }),
|
|
46
|
+
defaultModel: z.string().trim().min(1).max(256),
|
|
47
|
+
baseURL: providerBaseUrlSchema.optional(),
|
|
48
|
+
maxOutputTokens: z.number().int().min(128).max(16_384).default(DEFAULT_MAX_OUTPUT_TOKENS),
|
|
49
|
+
timeoutMs: z.number().int().min(1_000).max(120_000).default(DEFAULT_TIMEOUT_MS),
|
|
50
|
+
}).strict();
|
|
51
|
+
|
|
52
|
+
const textGenerationInputSchema = z.object({
|
|
53
|
+
prompt: z.string().min(1),
|
|
54
|
+
systemPrompt: z.string().optional(),
|
|
55
|
+
model: z.string().trim().min(1).max(256).optional(),
|
|
56
|
+
maxOutputTokens: z.number().int().min(128).max(16_384).optional(),
|
|
57
|
+
}).strict();
|
|
58
|
+
|
|
59
|
+
export type TextGenerationProviderRunInput = {
|
|
60
|
+
provider: TextGenerationProvider;
|
|
61
|
+
config: ResolvedProviderConfig;
|
|
62
|
+
input: HarnessModelRunInput;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type TextGenerationResourceDependencies = {
|
|
66
|
+
runProvider?: (input: TextGenerationProviderRunInput) => Promise<NormalizedModelOutput>;
|
|
67
|
+
now?: () => number;
|
|
68
|
+
defaultAbortSignal?: AbortSignal;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function byteLength(value: string): number {
|
|
72
|
+
return new TextEncoder().encode(value).byteLength;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function durationSince(startedAt: number, now: () => number): number {
|
|
76
|
+
return Math.max(0, Math.round(now() - startedAt));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function failure(input: {
|
|
80
|
+
provider: TextGenerationProvider;
|
|
81
|
+
model: string;
|
|
82
|
+
startedAt: number;
|
|
83
|
+
now: () => number;
|
|
84
|
+
code: TextGenerationErrorCode;
|
|
85
|
+
message: string;
|
|
86
|
+
retryable: boolean;
|
|
87
|
+
}): ProviderTextGenerationResult {
|
|
88
|
+
return {
|
|
89
|
+
success: false,
|
|
90
|
+
provider: input.provider,
|
|
91
|
+
model: input.model,
|
|
92
|
+
durationMs: durationSince(input.startedAt, input.now),
|
|
93
|
+
error: {
|
|
94
|
+
code: input.code,
|
|
95
|
+
message: input.message,
|
|
96
|
+
retryable: input.retryable,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function readHttpStatus(error: unknown): number | undefined {
|
|
102
|
+
const visited = new Set<unknown>();
|
|
103
|
+
let current: unknown = error;
|
|
104
|
+
while (current && typeof current === 'object' && !visited.has(current)) {
|
|
105
|
+
visited.add(current);
|
|
106
|
+
const status = 'status' in current ? (current as { status?: unknown }).status : undefined;
|
|
107
|
+
if (typeof status === 'number' && Number.isFinite(status)) {
|
|
108
|
+
return status;
|
|
109
|
+
}
|
|
110
|
+
current = 'cause' in current ? (current as { cause?: unknown }).cause : undefined;
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function classifyProviderError(error: unknown): {
|
|
116
|
+
code: TextGenerationErrorCode;
|
|
117
|
+
message: string;
|
|
118
|
+
retryable: boolean;
|
|
119
|
+
} {
|
|
120
|
+
if (error instanceof ModelOutputError) {
|
|
121
|
+
const isCompletedEmpty = error.diagnostic.stage === 'response_completion'
|
|
122
|
+
&& [
|
|
123
|
+
'completed_empty',
|
|
124
|
+
'end_turn',
|
|
125
|
+
'stop_sequence',
|
|
126
|
+
'tool_use',
|
|
127
|
+
'refusal',
|
|
128
|
+
'missing',
|
|
129
|
+
].includes(error.diagnostic.completionReason);
|
|
130
|
+
return isCompletedEmpty
|
|
131
|
+
? {
|
|
132
|
+
code: 'TEXT_GENERATION_EMPTY_RESPONSE',
|
|
133
|
+
message: '模型服务返回了空文本',
|
|
134
|
+
retryable: false,
|
|
135
|
+
}
|
|
136
|
+
: {
|
|
137
|
+
code: 'TEXT_GENERATION_PROVIDER_ERROR',
|
|
138
|
+
message: '模型服务返回了非预期错误',
|
|
139
|
+
retryable: true,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const status = readHttpStatus(error);
|
|
143
|
+
if (status === 400 || status === 404 || status === 422) {
|
|
144
|
+
return {
|
|
145
|
+
code: 'TEXT_GENERATION_INVALID_REQUEST',
|
|
146
|
+
message: '模型服务拒绝了生成请求,请检查平台模型配置',
|
|
147
|
+
retryable: false,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (status === 401 || status === 403) {
|
|
151
|
+
return {
|
|
152
|
+
code: 'TEXT_GENERATION_AUTH_FAILED',
|
|
153
|
+
message: '模型服务认证失败,请检查平台模型凭据',
|
|
154
|
+
retryable: false,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (status === 429) {
|
|
158
|
+
return {
|
|
159
|
+
code: 'TEXT_GENERATION_RATE_LIMITED',
|
|
160
|
+
message: '模型服务请求过于频繁,请稍后重试',
|
|
161
|
+
retryable: true,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (status !== undefined && status >= 500) {
|
|
165
|
+
return {
|
|
166
|
+
code: 'TEXT_GENERATION_PROVIDER_UNAVAILABLE',
|
|
167
|
+
message: '模型服务暂时不可用,请稍后重试',
|
|
168
|
+
retryable: true,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
code: 'TEXT_GENERATION_PROVIDER_ERROR',
|
|
173
|
+
message: '模型服务返回了非预期错误',
|
|
174
|
+
retryable: status === undefined && !(error instanceof ModelProviderError),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function abortError(reason?: unknown): Error {
|
|
179
|
+
if (reason instanceof Error && reason.name === 'AbortError') {
|
|
180
|
+
return reason;
|
|
181
|
+
}
|
|
182
|
+
return new DOMException('aborted', 'AbortError');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function defaultRunProvider(
|
|
186
|
+
input: TextGenerationProviderRunInput,
|
|
187
|
+
): Promise<NormalizedModelOutput> {
|
|
188
|
+
return input.provider === 'openai'
|
|
189
|
+
? runOpenAI(input.input, input.config)
|
|
190
|
+
: runAnthropic(input.input, input.config);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function buildModelInput(input: Pick<TextGenerationInput, 'prompt' | 'systemPrompt'>, abortSignal: AbortSignal): HarnessModelRunInput {
|
|
194
|
+
const systemPrompt = input.systemPrompt?.trim() ?? '';
|
|
195
|
+
return {
|
|
196
|
+
context: {
|
|
197
|
+
messages: [
|
|
198
|
+
...(systemPrompt
|
|
199
|
+
? [{
|
|
200
|
+
role: 'system' as const,
|
|
201
|
+
content: [{ type: 'text' as const, text: systemPrompt }],
|
|
202
|
+
}]
|
|
203
|
+
: []),
|
|
204
|
+
{
|
|
205
|
+
role: 'user',
|
|
206
|
+
content: [{ type: 'text', text: input.prompt }],
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
},
|
|
210
|
+
systemPrompt,
|
|
211
|
+
tools: [],
|
|
212
|
+
abortSignal,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* 创建不进入 Harness Agent loop 的单轮文本生成 Resource。
|
|
218
|
+
*
|
|
219
|
+
* 内部执行器只接收平台工厂解析的实际目标与预算;公开档位校验由平台工厂承担。
|
|
220
|
+
*/
|
|
221
|
+
export function createProviderTextGenerationResource(
|
|
222
|
+
rawOptions: TextGenerationOptions,
|
|
223
|
+
dependencies: TextGenerationResourceDependencies = {},
|
|
224
|
+
): ProviderTextGenerationResource {
|
|
225
|
+
const options = textGenerationOptionsSchema.parse(rawOptions);
|
|
226
|
+
const runProvider = dependencies.runProvider ?? defaultRunProvider;
|
|
227
|
+
const now = dependencies.now ?? Date.now;
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
async generate(rawInput, context) {
|
|
231
|
+
const startedAt = now();
|
|
232
|
+
const candidateModel = typeof rawInput?.model === 'string' && rawInput.model.trim()
|
|
233
|
+
? rawInput.model.trim()
|
|
234
|
+
: options.defaultModel;
|
|
235
|
+
const parsedInput = textGenerationInputSchema.safeParse(rawInput);
|
|
236
|
+
if (!parsedInput.success) {
|
|
237
|
+
return failure({
|
|
238
|
+
provider: options.provider,
|
|
239
|
+
model: candidateModel,
|
|
240
|
+
startedAt,
|
|
241
|
+
now,
|
|
242
|
+
code: 'TEXT_GENERATION_INVALID_REQUEST',
|
|
243
|
+
message: '文本生成请求不合法',
|
|
244
|
+
retryable: false,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
const input = parsedInput.data;
|
|
248
|
+
const model = input.model ?? options.defaultModel;
|
|
249
|
+
if (
|
|
250
|
+
byteLength(input.prompt) > MAX_PROMPT_BYTES
|
|
251
|
+
|| byteLength(input.systemPrompt ?? '') > MAX_SYSTEM_PROMPT_BYTES
|
|
252
|
+
) {
|
|
253
|
+
return failure({
|
|
254
|
+
provider: options.provider,
|
|
255
|
+
model,
|
|
256
|
+
startedAt,
|
|
257
|
+
now,
|
|
258
|
+
code: 'TEXT_GENERATION_INVALID_REQUEST',
|
|
259
|
+
message: '文本生成输入超过 Resource 安全上限',
|
|
260
|
+
retryable: false,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
const maxOutputTokens = input.maxOutputTokens ?? options.maxOutputTokens;
|
|
264
|
+
if (maxOutputTokens > options.maxOutputTokens) {
|
|
265
|
+
return failure({
|
|
266
|
+
provider: options.provider,
|
|
267
|
+
model,
|
|
268
|
+
startedAt,
|
|
269
|
+
now,
|
|
270
|
+
code: 'TEXT_GENERATION_INVALID_REQUEST',
|
|
271
|
+
message: '调用级输出预算不能超过 Environment 上限',
|
|
272
|
+
retryable: false,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const upstreamSignal = context?.abortSignal ?? dependencies.defaultAbortSignal;
|
|
277
|
+
if (upstreamSignal?.aborted) {
|
|
278
|
+
throw abortError(upstreamSignal.reason);
|
|
279
|
+
}
|
|
280
|
+
const controller = new AbortController();
|
|
281
|
+
let timedOut = false;
|
|
282
|
+
const onUpstreamAbort = () => controller.abort(upstreamSignal?.reason);
|
|
283
|
+
upstreamSignal?.addEventListener('abort', onUpstreamAbort, { once: true });
|
|
284
|
+
const timer = setTimeout(() => {
|
|
285
|
+
timedOut = true;
|
|
286
|
+
controller.abort();
|
|
287
|
+
}, options.timeoutMs);
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
const output = await runProvider({
|
|
291
|
+
provider: options.provider,
|
|
292
|
+
config: {
|
|
293
|
+
model,
|
|
294
|
+
apiKey: options.apiKey,
|
|
295
|
+
...(options.baseURL ? { baseURL: options.baseURL } : {}),
|
|
296
|
+
maxOutputTokens,
|
|
297
|
+
},
|
|
298
|
+
input: buildModelInput(input, controller.signal),
|
|
299
|
+
});
|
|
300
|
+
const text = output.text.trim();
|
|
301
|
+
if (!text) {
|
|
302
|
+
return failure({
|
|
303
|
+
provider: options.provider,
|
|
304
|
+
model,
|
|
305
|
+
startedAt,
|
|
306
|
+
now,
|
|
307
|
+
code: 'TEXT_GENERATION_EMPTY_RESPONSE',
|
|
308
|
+
message: '模型服务返回了空文本',
|
|
309
|
+
retryable: false,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
if (output.toolCalls.length > 0 || byteLength(text) > MAX_RESPONSE_BYTES) {
|
|
313
|
+
return failure({
|
|
314
|
+
provider: options.provider,
|
|
315
|
+
model,
|
|
316
|
+
startedAt,
|
|
317
|
+
now,
|
|
318
|
+
code: 'TEXT_GENERATION_PROVIDER_ERROR',
|
|
319
|
+
message: '模型服务返回了不符合单轮文本契约的结果',
|
|
320
|
+
retryable: false,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
success: true,
|
|
325
|
+
provider: options.provider,
|
|
326
|
+
model,
|
|
327
|
+
text,
|
|
328
|
+
durationMs: durationSince(startedAt, now),
|
|
329
|
+
};
|
|
330
|
+
} catch (error) {
|
|
331
|
+
if (upstreamSignal?.aborted) {
|
|
332
|
+
throw abortError(upstreamSignal.reason ?? error);
|
|
333
|
+
}
|
|
334
|
+
if (timedOut) {
|
|
335
|
+
return failure({
|
|
336
|
+
provider: options.provider,
|
|
337
|
+
model,
|
|
338
|
+
startedAt,
|
|
339
|
+
now,
|
|
340
|
+
code: 'TEXT_GENERATION_TIMEOUT',
|
|
341
|
+
message: '文本生成超时,请稍后重试',
|
|
342
|
+
retryable: true,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
const classified = classifyProviderError(error);
|
|
346
|
+
return failure({
|
|
347
|
+
provider: options.provider,
|
|
348
|
+
model,
|
|
349
|
+
startedAt,
|
|
350
|
+
now,
|
|
351
|
+
...classified,
|
|
352
|
+
});
|
|
353
|
+
} finally {
|
|
354
|
+
clearTimeout(timer);
|
|
355
|
+
upstreamSignal?.removeEventListener('abort', onUpstreamAbort);
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
type StripTier<T> = T extends unknown ? Omit<T, 'modelTier'> : never;
|
|
363
|
+
type ProviderTextGenerationResult = StripTier<TextGenerationResult>;
|
|
364
|
+
type ProviderTextGenerationResource = { generate(input: Omit<TextGenerationInput, 'model'> & { model?: string }, context?: TextGenerationRequestContext): Promise<ProviderTextGenerationResult> };
|