@henjicc/ai-sdk 0.5.1 → 0.5.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.2 - 2026-09-21
4
+
5
+ - `llm/streaming` 新增有类型定义的文本、JSON Object 与 JSON Schema 输出配置,并把它正式写入 Chat Completions 的 `response_format`。
6
+ - 模型能力现在可声明结构化输出与思考模式能否并用;不支持或尚未核实的组合在网络请求前返回可解析错误码,Groq 流式 JSON Schema 按官方限制明确拒绝。
7
+ - 新增 `maxOutputTokens`,OpenAI/Groq/MiMo 使用 `max_completion_tokens`,其余兼容端点沿用 `max_tokens`;省略时不再强制 4096,流结束用 `truncated` 标记 token 上限截断。
8
+ - 最终发送体复用正式构建结果,`onRequestBuilt` 只收到观察快照;保留正文/思考分流、AbortSignal、可选总时限与轻量受限宿主边界。
9
+
3
10
  ## 0.5.1 - 2026-09-21
4
11
 
5
12
  - Grsai GPT Image 2.5 默认变体从维护中的 Flare 切换为可用的 Standard;显式 Flare/Sunburst 参数保持兼容,等待官方恢复公告。
package/README.md CHANGED
@@ -48,10 +48,10 @@ try {
48
48
 
49
49
  百炼工厂必须传 `baseUrl` 为实际地域/工作空间的 API 根地址,不包含端点路径。其他供应商可以覆盖根地址与 `credentialId`;SDK 不自动切换地域或账号。不同模型的向量不能混用;切换模型通常需要重建向量索引。Rerank 分数仅在本次请求内比较。
50
50
 
51
- SDK `0.5.1` 的正式分发渠道为公共 npm,无需配置 registry 或访问令牌:
51
+ SDK `0.5.2` 的正式分发渠道为公共 npm,无需配置 registry 或访问令牌:
52
52
 
53
53
  ```bash
54
- npm install @henjicc/ai-sdk@0.5.1
54
+ npm install @henjicc/ai-sdk@0.5.2
55
55
  ```
56
56
 
57
57
  然后提供 4 个宿主能力(`Transport` / `CredentialStore` / `MediaReader` / `Logger`),创建客户端:
@@ -192,14 +192,50 @@ import {
192
192
  type RuntimeContext,
193
193
  } from '@henjicc/ai-sdk/llm/streaming'
194
194
 
195
- const request: LlmChatRequestDto = {
196
- requestId: 'chat-1', providerId: 'openai', modelId: 'gpt-5-mini',
197
- messages: [{ role: 'user', content: '你好' }],
198
- }
199
- const result = await runLlmChatStream(request, 'chat-1', onStreamEvent, runtime as RuntimeContext)
200
- console.log(result.output, result.reasoningOutput, result.usage, result.finishReason)
201
- cancelLlmChatTask('chat-1')
202
- ```
195
+ const request: LlmChatRequestDto = {
196
+ requestId: 'chat-1', providerId: 'openai', modelId: 'gpt-5-mini',
197
+ messages: [{ role: 'user', content: '你好' }],
198
+ capabilities: {
199
+ structuredOutputMode: 'schema',
200
+ reasoning: true,
201
+ structuredOutputWithReasoning: true,
202
+ maxOutputTokens: 32_000,
203
+ },
204
+ reasoning: { enabled: true, effort: 'high' },
205
+ structuredOutput: {
206
+ type: 'json_schema',
207
+ name: 'subtitle_segments',
208
+ strict: true,
209
+ schema: {
210
+ type: 'object',
211
+ properties: {
212
+ segments: {
213
+ type: 'array',
214
+ items: {
215
+ type: 'object',
216
+ properties: { text: { type: 'string' } },
217
+ required: ['text'],
218
+ additionalProperties: false,
219
+ },
220
+ },
221
+ },
222
+ required: ['segments'],
223
+ additionalProperties: false,
224
+ },
225
+ },
226
+ maxOutputTokens: 24_000,
227
+ }
228
+ const result = await runLlmChatStream(request, 'chat-1', onStreamEvent, runtime as RuntimeContext)
229
+ console.log(result.output, result.reasoningOutput, result.usage, result.finishReason, result.truncated)
230
+ cancelLlmChatTask('chat-1')
231
+ ```
232
+
233
+ `structuredOutput` 支持 `text`、`json_object`、`json_schema`。轻量入口只执行 Chat Completions,
234
+ 所以这里会正式映射到最终请求体的 `response_format`;它不包含 Responses API 执行器,Responses
235
+ 路径的等价字段 `text.format` 由完整 `llm` 入口处理。JSON Object、JSON Schema 和“结构化输出 +
236
+ 思考”都必须由 `capabilities` 明确声明;未知或不支持的组合会在网络请求前返回结构化错误,不会静默
237
+ 降级。省略 `maxOutputTokens` 时 SDK 不再强加 4096 tokens,而是交给供应商默认值;省略执行参数里的
238
+ `timeoutMs` 则没有请求总时限,宿主可在 `Transport` 中实现长时间无数据才超时。
203
239
 
204
240
  ### 可选模型分发包与统一能力筛选
205
241
 
@@ -28,10 +28,11 @@ export interface LlmChatCompletedInfo {
28
28
  reasoningOutput: string;
29
29
  usage: LlmUsageDto | null;
30
30
  finishReason: string | null;
31
+ truncated: boolean;
31
32
  toolCalls?: LlmStreamToolCall[];
32
33
  }
33
34
  export interface LlmChatStreamHooks {
34
- /** 请求体与 endpoint 已经构建好,即将发出网络请求前触发一次。 */
35
+ /** 请求体与 endpoint 已经构建好,即将发出网络请求前触发一次;requestPayload 是只用于观察的快照。 */
35
36
  onRequestBuilt?: (info: LlmChatRequestBuiltInfo) => void;
36
37
  /** 流式响应正常读完(未被取消、未抛错)后触发一次。 */
37
38
  onCompleted?: (info: LlmChatCompletedInfo) => void;
@@ -47,6 +48,7 @@ export interface LlmChatStreamOutcome {
47
48
  reasoningOutput: string;
48
49
  usage: LlmUsageDto | null;
49
50
  finishReason: string | null;
51
+ truncated: boolean;
50
52
  toolCalls?: LlmStreamToolCall[];
51
53
  }
52
54
  export interface LlmChatExecutionOptions {
package/dist/llm/chat.js CHANGED
@@ -53,11 +53,16 @@ export async function runLlmChatStream(request, taskId, emit, runtime, hooks = {
53
53
  ? resolvePpioChatEndpoint(processedRequest.baseUrl)
54
54
  : resolveOpenAiCompatibleEndpoint(processedRequest);
55
55
  const requestPayload = buildOpenAiCompatiblePayload(processedRequest);
56
- hooks.onRequestBuilt?.({ endpoint, requestPayload, processedRequest });
56
+ hooks.onRequestBuilt?.({
57
+ endpoint,
58
+ requestPayload: cloneJsonObject(requestPayload),
59
+ processedRequest,
60
+ });
57
61
  const output = await streamOpenAiCompatibleChat({
58
62
  endpoint,
59
63
  apiKey,
60
- request: processedRequest,
64
+ providerId: identity.providerFamilyId,
65
+ payload: requestPayload,
61
66
  signal: controller.signal,
62
67
  emit,
63
68
  transport: runtime.transport,
@@ -77,6 +82,7 @@ export async function runLlmChatStream(request, taskId, emit, runtime, hooks = {
77
82
  reasoningOutput: output.reasoningOutput,
78
83
  usage: output.usage,
79
84
  finishReason: output.finishReason,
85
+ truncated: output.truncated,
80
86
  toolCalls: output.toolCalls,
81
87
  });
82
88
  span?.end();
@@ -91,6 +97,7 @@ export async function runLlmChatStream(request, taskId, emit, runtime, hooks = {
91
97
  reasoningOutput: output.reasoningOutput,
92
98
  usage: output.usage,
93
99
  finishReason: output.finishReason,
100
+ truncated: output.truncated,
94
101
  toolCalls: output.toolCalls,
95
102
  };
96
103
  }
@@ -142,3 +149,6 @@ function isAbortError(error) {
142
149
  function isJsonObject(value) {
143
150
  return typeof value === 'object' && value !== null && !Array.isArray(value);
144
151
  }
152
+ function cloneJsonObject(value) {
153
+ return JSON.parse(JSON.stringify(value));
154
+ }
@@ -1,5 +1,6 @@
1
1
  import type { JsonObject, JsonValue } from '../types/runtime.js';
2
2
  import type { LlmReasoningConfig } from './reasoning.js';
3
+ import type { LlmCapabilities, LlmStructuredOutputConfig } from './types.js';
3
4
  export type { JsonObject, JsonValue };
4
5
  /**
5
6
  * 原生 SSE 流式聊天路径(`llm:chatStream`)的请求/响应 DTO。
@@ -34,7 +35,10 @@ export interface LlmChatRequestDto {
34
35
  baseUrl?: string;
35
36
  reasoning?: LlmReasoningConfig;
36
37
  messages: LlmChatMessageDto[];
37
- capabilities?: JsonObject;
38
+ capabilities?: Partial<LlmCapabilities>;
39
+ structuredOutput?: LlmStructuredOutputConfig;
40
+ /** 正整数;不设置时不下发限制,由供应商或模型默认值决定。 */
41
+ maxOutputTokens?: number;
38
42
  tools?: JsonValue;
39
43
  policy?: JsonObject;
40
44
  memory?: JsonObject;
@@ -75,6 +79,8 @@ export interface LlmStreamOutput {
75
79
  reasoningOutput: string;
76
80
  usage: LlmUsageDto | null;
77
81
  finishReason: string | null;
82
+ /** 供应商明确因输出 token 上限结束时为 true。 */
83
+ truncated: boolean;
78
84
  toolCalls?: LlmStreamToolCall[];
79
85
  }
80
86
  export interface LlmStreamToolCall {
@@ -28,6 +28,7 @@ export const DEFAULT_LLM_CAPABILITIES = {
28
28
  parallelTools: false,
29
29
  jsonOutput: false,
30
30
  structuredOutputMode: 'none',
31
+ structuredOutputWithReasoning: null,
31
32
  reasoning: false,
32
33
  sampling: true,
33
34
  contextWindow: null,
@@ -39,6 +39,7 @@ export function createGroqLlmModule() {
39
39
  reasoningOutput: outcome.reasoningOutput,
40
40
  usage: outcome.usage,
41
41
  finishReason: outcome.finishReason,
42
+ truncated: outcome.truncated,
42
43
  toolCalls: outcome.toolCalls,
43
44
  };
44
45
  },
@@ -40,6 +40,7 @@ export const GROQ_DEFAULT_MODEL_CONFIG = {
40
40
  parallelTools: false,
41
41
  jsonOutput: true,
42
42
  structuredOutputMode: 'json',
43
+ structuredOutputWithReasoning: true,
43
44
  reasoning: true,
44
45
  sampling: true,
45
46
  contextWindow: 131_072,
@@ -28,6 +28,8 @@ export interface LlmModelCatalogEntry {
28
28
  toolCall: boolean;
29
29
  parallelTools: boolean;
30
30
  structuredOutputMode: LlmCapabilities['structuredOutputMode'];
31
+ /** 结构化输出与思考模式能否同时启用;省略表示官方资料尚未核实。 */
32
+ structuredOutputWithReasoning?: boolean;
31
33
  reasoning: boolean;
32
34
  /** 是否接受 temperature / top_p;官方声明为固定值的模型要记 false */
33
35
  sampling: boolean;
@@ -44,6 +44,7 @@ export function applyLlmModelCatalogEntry(base, entry) {
44
44
  toolCall: entry.toolCall,
45
45
  parallelTools: entry.parallelTools,
46
46
  structuredOutputMode: entry.structuredOutputMode,
47
+ structuredOutputWithReasoning: entry.structuredOutputWithReasoning ?? null,
47
48
  jsonOutput: entry.structuredOutputMode !== 'none',
48
49
  reasoning: entry.reasoning,
49
50
  sampling: entry.sampling,
@@ -37,6 +37,7 @@ export const LLM_MODEL_CATALOG_ENTRIES = [
37
37
  toolCall: true,
38
38
  parallelTools: false,
39
39
  structuredOutputMode: 'json',
40
+ structuredOutputWithReasoning: true,
40
41
  reasoning: true,
41
42
  sampling: true,
42
43
  contextWindow: 131_072,
@@ -117,6 +118,7 @@ export const LLM_MODEL_CATALOG_ENTRIES = [
117
118
  toolCall: true,
118
119
  parallelTools: false,
119
120
  structuredOutputMode: 'schema',
121
+ structuredOutputWithReasoning: true,
120
122
  reasoning: true,
121
123
  // 官方文档把 temperature / top_p 等列为固定值并明确"建议不要显式传入",
122
124
  // sampling=false 会让模型步骤不再下发这两个参数。
@@ -55,8 +55,8 @@ export type ModelStepTool = z.infer<typeof modelStepToolSchema>;
55
55
  export declare const modelStepTraceMetadataSchema: z.ZodObject<{
56
56
  kind: z.ZodEnum<{
57
57
  fallback: "fallback";
58
- router: "router";
59
58
  primary: "primary";
59
+ router: "router";
60
60
  summarizer: "summarizer";
61
61
  observer: "observer";
62
62
  other: "other";
@@ -174,8 +174,8 @@ export declare const modelStepInputSchema: z.ZodObject<{
174
174
  trace: z.ZodOptional<z.ZodObject<{
175
175
  kind: z.ZodEnum<{
176
176
  fallback: "fallback";
177
- router: "router";
178
177
  primary: "primary";
178
+ router: "router";
179
179
  summarizer: "summarizer";
180
180
  observer: "observer";
181
181
  other: "other";
@@ -7,8 +7,9 @@ export function resolveProviderExtraAuthHeaders(providerId, apiKey) {
7
7
  }
8
8
  export function applyProviderRequestBodyQuirks(providerId, body) {
9
9
  const normalizedProviderId = providerId.trim().toLowerCase();
10
- if (normalizedProviderId === 'mimo')
10
+ if (normalizedProviderId === 'mimo' || normalizedProviderId === 'openai') {
11
11
  return renameMaxCompletionTokens(body);
12
+ }
12
13
  if (normalizedProviderId !== 'groq')
13
14
  return body;
14
15
  const withTokenLimit = renameMaxCompletionTokens(body);
@@ -7,7 +7,7 @@
7
7
  export { cancelLlmChatTask, normalizeLlmChatError, resolveLlmTaskId, runLlmChatStream, } from '../chat.js';
8
8
  export type { LlmChatCompletedInfo, LlmChatRequestBuiltInfo, LlmChatStreamHooks, LlmChatStreamOutcome, } from '../chat.js';
9
9
  export type { JsonObject, JsonValue, LlmChatMessageDto, LlmChatRequestDto, LlmContentPart, LlmRole, LlmStreamEmitter, LlmStreamEventDto, LlmStreamOutput, LlmTraceDto, LlmUsageDto, } from '../chatTypes.js';
10
- export type { LlmReasoningConfig, LlmReasoningEffort } from '../reasoning.js';
10
+ export type { LlmReasoningConfig, LlmReasoningEffort, LlmStructuredOutputConfig, } from '../types.js';
11
11
  export { parseModelProviderError, ProviderModelStepError, serializeModelProviderError, } from '../../runtime/provider-error-core.js';
12
12
  export type { ModelProviderError, ModelProviderErrorCategory, } from '../../runtime/provider-error-core.js';
13
13
  export type { RuntimeContext } from '../../runtime/RuntimeContext.js';
@@ -12,7 +12,8 @@ import type { JsonObject, LlmChatMessageDto, LlmChatRequestDto, LlmStreamEmitter
12
12
  interface StreamChatOptions {
13
13
  endpoint: string;
14
14
  apiKey: string;
15
- request: LlmChatRequestDto;
15
+ providerId: string;
16
+ payload: JsonObject;
16
17
  signal: AbortSignal;
17
18
  emit: LlmStreamEmitter;
18
19
  transport: Transport;
@@ -25,15 +25,15 @@ export function resolveOpenAiCompatibleEndpoint(request) {
25
25
  : `${normalized}/v1/chat/completions`;
26
26
  }
27
27
  export function buildOpenAiCompatiblePayload(request) {
28
- const policy = request.policy ?? {};
29
28
  const payload = {
30
29
  model: request.modelId,
31
30
  messages: request.messages.map(serializeMessage),
32
31
  stream: true,
33
32
  stream_options: { include_usage: true },
34
33
  };
35
- const maxTokens = readNumber(policy.max_tokens ?? policy.maxTokens);
36
- payload.max_tokens = maxTokens ?? 4096;
34
+ const maxTokens = resolveMaxOutputTokens(request);
35
+ if (maxTokens !== undefined)
36
+ payload.max_tokens = maxTokens;
37
37
  if (request.tools !== undefined) {
38
38
  payload.tools = request.tools;
39
39
  }
@@ -45,23 +45,23 @@ export function buildOpenAiCompatiblePayload(request) {
45
45
  * 用模型能力表兜一层,没标"支持思考"的模型仍然一个字段都不发。
46
46
  */
47
47
  const identity = resolveLlmEndpointIdentity(request);
48
+ const withStructuredOutput = applyStructuredOutputRequestBody(request, identity.providerFamilyId, payload);
48
49
  const reasoningCapable = request.capabilities?.reasoning === true;
49
50
  const withReasoning = reasoningCapable
50
- ? applyProviderReasoningRequestBody(identity.providerFamilyId, request.adapter, payload, request.reasoning)
51
- : payload;
51
+ ? applyProviderReasoningRequestBody(identity.providerFamilyId, request.adapter, withStructuredOutput, request.reasoning)
52
+ : withStructuredOutput;
52
53
  return applyProviderRequestBodyQuirks(identity.providerFamilyId, withReasoning);
53
54
  }
54
55
  export async function streamOpenAiCompatibleChat(options) {
55
- const identity = resolveLlmEndpointIdentity(options.request);
56
- const response = await fetchProvider(identity.providerFamilyId, options.endpoint, {
56
+ const response = await fetchProvider(options.providerId, options.endpoint, {
57
57
  method: 'POST',
58
58
  headers: {
59
59
  Accept: 'text/event-stream',
60
60
  Authorization: `Bearer ${options.apiKey}`,
61
- ...resolveProviderExtraAuthHeaders(identity.providerFamilyId, options.apiKey),
61
+ ...resolveProviderExtraAuthHeaders(options.providerId, options.apiKey),
62
62
  'Content-Type': 'application/json',
63
63
  },
64
- body: JSON.stringify(buildOpenAiCompatiblePayload(options.request)),
64
+ body: JSON.stringify(options.payload),
65
65
  signal: options.signal,
66
66
  }, {
67
67
  transport: options.transport,
@@ -160,7 +160,14 @@ async function readSseStream(body, emit) {
160
160
  for (const event of parsed.events) {
161
161
  const chunk = parseSseData(event);
162
162
  if (chunk.done) {
163
- return { output, reasoningOutput, usage, finishReason, toolCalls: [...toolCalls.values()] };
163
+ return {
164
+ output,
165
+ reasoningOutput,
166
+ usage,
167
+ finishReason,
168
+ truncated: isOutputTruncated(finishReason),
169
+ toolCalls: [...toolCalls.values()],
170
+ };
164
171
  }
165
172
  usage = chunk.usage ?? usage;
166
173
  finishReason = chunk.finishReason ?? finishReason;
@@ -194,7 +201,89 @@ async function readSseStream(body, emit) {
194
201
  emit({ type: 'Token', data: chunk.content });
195
202
  }
196
203
  }
197
- return { output, reasoningOutput, usage, finishReason, toolCalls: [...toolCalls.values()] };
204
+ return {
205
+ output,
206
+ reasoningOutput,
207
+ usage,
208
+ finishReason,
209
+ truncated: isOutputTruncated(finishReason),
210
+ toolCalls: [...toolCalls.values()],
211
+ };
212
+ }
213
+ function applyStructuredOutputRequestBody(request, providerId, body) {
214
+ const output = request.structuredOutput;
215
+ if (!output)
216
+ return body;
217
+ if (output.type === 'text') {
218
+ return { ...body, response_format: { type: 'text' } };
219
+ }
220
+ const supportedMode = request.capabilities?.structuredOutputMode ?? 'none';
221
+ if (output.type === 'json_object' && supportedMode === 'none') {
222
+ throw invalidRequest('STRUCTURED_OUTPUT_JSON_OBJECT_UNSUPPORTED', `Model "${request.modelId}" is not declared to support JSON Object output.`);
223
+ }
224
+ if (output.type === 'json_schema' && supportedMode !== 'schema') {
225
+ throw invalidRequest('STRUCTURED_OUTPUT_JSON_SCHEMA_UNSUPPORTED', `Model "${request.modelId}" is not declared to support JSON Schema output.`);
226
+ }
227
+ if (request.reasoning?.enabled === true) {
228
+ if (request.capabilities?.reasoning !== true) {
229
+ throw invalidRequest('REASONING_UNSUPPORTED', `Model "${request.modelId}" is not declared to support reasoning.`);
230
+ }
231
+ const compatibility = request.capabilities.structuredOutputWithReasoning;
232
+ if (compatibility !== true) {
233
+ throw invalidRequest(compatibility === false
234
+ ? 'STRUCTURED_OUTPUT_WITH_REASONING_UNSUPPORTED'
235
+ : 'STRUCTURED_OUTPUT_WITH_REASONING_CAPABILITY_REQUIRED', compatibility === false
236
+ ? `Model "${request.modelId}" does not support structured output with reasoning enabled.`
237
+ : `Model "${request.modelId}" must explicitly declare structuredOutputWithReasoning before structured output and reasoning can be combined.`);
238
+ }
239
+ }
240
+ if (output.type === 'json_object') {
241
+ return { ...body, response_format: { type: 'json_object' } };
242
+ }
243
+ if (providerId.trim().toLowerCase() === 'groq') {
244
+ throw invalidRequest('STRUCTURED_OUTPUT_STREAMING_UNSUPPORTED', 'Groq does not support JSON Schema structured output with streaming.');
245
+ }
246
+ if (!output.name.trim()) {
247
+ throw invalidRequest('INVALID_STRUCTURED_OUTPUT', 'JSON Schema output requires a non-empty name.');
248
+ }
249
+ if (!isRecord(output.schema)) {
250
+ throw invalidRequest('INVALID_STRUCTURED_OUTPUT', 'JSON Schema output requires schema to be an object.');
251
+ }
252
+ if (typeof output.strict !== 'boolean') {
253
+ throw invalidRequest('INVALID_STRUCTURED_OUTPUT', 'JSON Schema output requires an explicit strict boolean.');
254
+ }
255
+ return {
256
+ ...body,
257
+ response_format: {
258
+ type: 'json_schema',
259
+ json_schema: {
260
+ name: output.name.trim(),
261
+ schema: output.schema,
262
+ strict: output.strict,
263
+ },
264
+ },
265
+ };
266
+ }
267
+ function resolveMaxOutputTokens(request) {
268
+ const policy = request.policy ?? {};
269
+ const legacy = policy.max_tokens ?? policy.maxTokens;
270
+ const candidate = request.maxOutputTokens ?? legacy;
271
+ if (candidate === undefined)
272
+ return undefined;
273
+ if (typeof candidate !== 'number' || !Number.isInteger(candidate) || candidate <= 0) {
274
+ throw invalidRequest('INVALID_MAX_OUTPUT_TOKENS', 'maxOutputTokens must be a positive integer.');
275
+ }
276
+ const modelLimit = request.capabilities?.maxOutputTokens;
277
+ if (typeof modelLimit === 'number' && candidate > modelLimit) {
278
+ throw invalidRequest('MAX_OUTPUT_TOKENS_EXCEEDED', `Requested ${candidate} output tokens, but model "${request.modelId}" declares a limit of ${modelLimit}.`);
279
+ }
280
+ return candidate;
281
+ }
282
+ function invalidRequest(code, message) {
283
+ return Object.assign(new Error(message), { code, statusCode: 400 });
284
+ }
285
+ function isOutputTruncated(finishReason) {
286
+ return finishReason === 'length' || finishReason === 'max_tokens' || finishReason === 'max_output_tokens';
198
287
  }
199
288
  function drainSseEvents(input) {
200
289
  const events = [];
@@ -312,9 +401,6 @@ function readDelta(value) {
312
401
  function readString(value) {
313
402
  return typeof value === 'string' && value.length > 0 ? value : undefined;
314
403
  }
315
- function readNumber(value) {
316
- return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
317
- }
318
404
  function readTokenCount(value) {
319
405
  return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null;
320
406
  }
@@ -1,6 +1,18 @@
1
1
  import type { LlmApiProtocol } from './providerProtocol.js';
2
2
  import type { LlmReasoningConfig } from './reasoning.js';
3
+ import type { JsonObject } from '../types/runtime.js';
3
4
  export type { LlmReasoningConfig, LlmReasoningEffort } from './reasoning.js';
5
+ /** Chat Completions 的正式输出格式配置;省略时由供应商使用默认文本格式。 */
6
+ export type LlmStructuredOutputConfig = {
7
+ type: 'text';
8
+ } | {
9
+ type: 'json_object';
10
+ } | {
11
+ type: 'json_schema';
12
+ name: string;
13
+ schema: JsonObject;
14
+ strict: boolean;
15
+ };
4
16
  export interface LlmCapabilities {
5
17
  text: boolean;
6
18
  image: boolean;
@@ -13,6 +25,11 @@ export interface LlmCapabilities {
13
25
  parallelTools: boolean;
14
26
  jsonOutput: boolean;
15
27
  structuredOutputMode: 'none' | 'json' | 'schema';
28
+ /**
29
+ * 结构化输出能否与思考模式同时启用。`null` / 缺省表示尚未核实;
30
+ * 轻量流式入口在调用方明确开启两者时会拒绝未知组合,避免静默丢参数。
31
+ */
32
+ structuredOutputWithReasoning?: boolean | null;
16
33
  reasoning: boolean;
17
34
  sampling: boolean;
18
35
  contextWindow: number | null;
@@ -282,6 +299,8 @@ export interface LlmChatRequest {
282
299
  reasoning?: LlmReasoningConfig;
283
300
  messages: LlmChatMessage[];
284
301
  capabilities?: Partial<LlmCapabilities>;
302
+ structuredOutput?: LlmStructuredOutputConfig;
303
+ maxOutputTokens?: number;
285
304
  tools?: LlmToolSchema[];
286
305
  policy?: Partial<LlmPolicy>;
287
306
  memory?: LlmMemoryScope;
package/docs/consumers.md CHANGED
@@ -3,13 +3,19 @@
3
3
  本清单是 `@henjicc/ai-sdk` 消费方的唯一维护入口,用于 SDK 发布后的跨仓升级协调。
4
4
  绝对路径仅描述当前开发机上的仓库位置,不进入 SDK 运行时代码、发布包或用户配置。
5
5
 
6
- 最后核对日期:2026-09-14
6
+ 最后核对日期:2026-09-21
7
7
 
8
8
  Henji-AI 安装包的 GPT Image 2.5 高分辨率报错归属:宿主智能比例预处理曾忽略联动过滤,将 `smart` 转成 KIE 仅限 1K 的 `27:16`,SDK 在请求前正确拒绝。修复位于应用公共预处理,按当前分辨率/渠道的合法选项匹配;Flare/Sunburst 的 1K/2K/4K 及 APIMart、Grsai 同类筛选已有定向覆盖。无需放宽 SDK 契约或发布 SDK;安装包需随应用更新才包含修复,现有版本可手动选合法比例规避。
9
9
 
10
- 当前仓内及公共 npm SDK 版本:`0.5.0`(2026-09-21 已发布并完成公共匿名回装)
11
-
12
- 发布运行时代码提交 `1333422e`,补充边界回归提交 `da37e4bc`;后者必需 CI `35535140722` 全部成功。12组修复撤销验证均检测到失败,恢复后定向测试通过。固定候选包经仓外回装后发布,公共 registry 匿名安装的 shasum/integrity 与候选包一致;Vite公开入口和无TextEncoder/TextDecoder的受限宿主消费通过。npm网页登录与发布二次验证已完成;发布后等待公共索引可读才执行匿名验证,没有重复发布。
10
+ 当前仓内及公共 npm SDK 版本:`0.5.1`(2026-09-21 已发布并完成公共匿名回装)
11
+
12
+ `0.5.1` 将 Grsai GPT Image 2.5 未指定变体时的默认值从官方公告仍在维护的 Flare 改为 Standard,并同步默认价格;显式选择 Flare / Sunburst 的调用保持兼容。公共 DTO、请求协议、宿主边界和其余模型未变。发布前 77 个测试文件 / 899 项测试、可移植性、构建、49 个 Vite 公开入口和受限宿主门禁通过;公共索引可读后,已在隔离用户 npm 配置的仓外目录匿名安装并完成 ESM 导入。
13
+
14
+ - `0.5.1` tarball:https://registry.npmjs.org/@henjicc/ai-sdk/-/ai-sdk-0.5.1.tgz
15
+ - `0.5.1` shasum:`40087e680f1b7de63a601d12758acb7280de305f`
16
+ - `0.5.1` integrity:`sha512-+L/mAmFU6zgJ0gdsIwy0T1Z9TDOABaxO3HgLsEvMUNbRQhs9K55golF1472EgSJsIDysScnUulFJfBVg0xsNBg==`
17
+
18
+ `0.5.0` 发布运行时代码提交 `1333422e`,补充边界回归提交 `da37e4bc`;后者必需 CI `35535140722` 全部成功。12组修复撤销验证均检测到失败,恢复后定向测试通过。固定候选包经仓外回装后发布,公共 registry 匿名安装的 shasum/integrity 与候选包一致;Vite公开入口和无TextEncoder/TextDecoder的受限宿主消费通过。npm网页登录与发布二次验证已完成;发布后等待公共索引可读才执行匿名验证,没有重复发布。
13
19
 
14
20
  - tarball:https://registry.npmjs.org/@henjicc/ai-sdk/-/ai-sdk-0.5.0.tgz
15
21
  - shasum:`1b66a7c54f4f86a01b73fdca68b1c63e53dcbe10`
@@ -1,11 +1,17 @@
1
1
  # llm-chat
2
2
 
3
- 用 `@henjicc/ai-sdk@0.4.1` 的 `llm/streaming` 按需入口走 OpenAI-compatible SSE 流式对话,
3
+ 用 `@henjicc/ai-sdk@0.5.2` 的 `llm/streaming` 按需入口走 OpenAI-compatible SSE 流式对话,
4
4
  不会把 generation、BigModel preset/models/pricing、Groq 或 LLM modules 打进示例 bundle;
5
5
  通用端点身份解析所需的 BigModel profiles 仍会保留。默认目标是
6
6
  DeepSeek `deepseek-v4-flash`,提示词要求只回复 `SDK OK`,并限制最多 16 tokens。示例显式传入
7
7
  `reasoning: { enabled: false, effort: 'high' }`,避免短输出预算被供应商默认思考过程占用;若流结束后
8
- 没有任何文本 token,示例会明确失败。
8
+ 没有任何文本 token,示例会明确失败。
9
+
10
+ 输出上限使用有类型定义的 `maxOutputTokens`。省略时 SDK 不再自动写入 4096;调用方可以按字幕长度
11
+ 和模型上限选择更大的值。`runLlmChatStream` 的执行参数不传 `timeoutMs` 时没有请求总时限,CEP
12
+ 网络适配可在 `Transport` 中实现“长时间无数据才超时”。正式结构化输出可通过
13
+ `structuredOutput` 选择 `text`、`json_object` 或带 `name/schema/strict` 的 `json_schema`,并通过
14
+ `capabilities.structuredOutputMode` 与 `structuredOutputWithReasoning` 明确声明模型能力。
9
15
 
10
16
  ```bash
11
17
  npm install
@@ -68,7 +68,7 @@ async function main(): Promise<void> {
68
68
  reasoning: providerId === 'deepseek'
69
69
  ? { enabled: false, effort: 'high' }
70
70
  : undefined,
71
- policy: { max_tokens: 16 },
71
+ maxOutputTokens: 16,
72
72
  }
73
73
  const taskId = resolveLlmTaskId(request)
74
74
  let output = ''
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henjicc/ai-sdk",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "痕迹AI 多供应商模型适配 SDK",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {