Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ WebUI 保存的 `secret` 遵循环境变量名格式 `[A-Za-z_][A-Za-z0-9_]*`。
`include` 取回,所以只有 `encrypted` 形态的请求带 `include: ['reasoning.encrypted_content']`;
不认 `include` 的端点因此只在这一形态上会拒收整条请求。项的 `id` 是签发它的服务端的资源 id:来源实例、
模块或兼容域与本次请求不同的 message、function_call、function_call_output 出线时不带 `id`,
`call_id` 照带;同一来源和没有记录来源的项(Core 合成的 `msg_` / `fc_` / `fco_`)保留 `id`。模块的 `detect`
`call_id` 照带;同一来源和没有记录来源的项(Core 合成的 `msg_` / `fc_` / `fco_`)保留 `id`。function_call 的 `name`
是模型写出的字符串,其中 `[a-zA-Z0-9_.-]` 以外的字符出线时换成 `_`(端点遇到一个不收的字符就拒收整条请求);
session 记录不改,调用和回执仍按 `call_id` 配对。模块的 `detect`
探测发两条诊断请求(合成调用不带 / 带明文推理),按上游接受哪种写回 `reasoningReplay`。
`options.endpointPath`、
`options.extraHeaders`、`options.extraBody` 分别改路径、加头、并进请求体(`extraBody` 最后
Expand Down
3 changes: 2 additions & 1 deletion src/providers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ Core 侧:`activeProviderEntry()` / `activeSpec()` 每次现读;`contextWindowOf(
`responses-input.ts` 是原生 Responses 的无状态重放(system / developer 上提为
`instructions`;思维链按 `reasoningReplay` 以签名块或 `reasoning_text` 回传,明文形态下没有记录来源
的调用前补一项合成推理,`sendSyntheticReasoning` 为 false 时不补;来源实例、模块或兼容域与本次请求不同的 message /
function_call / function_call_output 出线时去掉可选的 `id`,`call_id` 不动,没有来源的项照带 `id`);`chat.ts` + `native-input.ts` / `history.ts` 转换 Chat Completions 请求(历史
function_call / function_call_output 出线时去掉可选的 `id`,`call_id` 不动,没有来源的项照带 `id`;function_call 的 `name` 里
`[a-zA-Z0-9_.-]` 以外的字符换成 `_`);`chat.ts` + `native-input.ts` / `history.ts` 转换 Chat Completions 请求(历史
思维链回不回传由模块定:`renderMessagesWithMedia` 的 `keepReasoning` 决定出不出线,`dropPastThinking`
把合成开头之外的每条抹空);
`response-assembly.ts` 把两种流归一成 Open Responses 的 Item 流,
Expand Down
10 changes: 10 additions & 0 deletions src/providers/transport/responses-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ export const SYNTHETIC_REASONING_TEXT = '-';
/** Item types whose `id` is optional on input; the id is the issuing server's resource id. */
const OPTIONAL_ID_TYPES: ReadonlySet<unknown> = new Set(['message', 'function_call', 'function_call_output']);

/**
* Characters outside the set a Responses endpoint accepts in a function call's `name`. The hosted
* service states the set in its rejection: `string does not match pattern '^[a-zA-Z0-9_.-]+$'`.
*/
const INVALID_NAME_CHARS = /[^a-zA-Z0-9_.-]/g;

export interface ResponsesInputOptions {
media?: CompatMediaOptions;
/** Whether past reasoning re-enters the context; the first synthetic turn is exempt. */
Expand All @@ -48,6 +54,9 @@ export interface ResponsesInputOptions {
* An item recorded from another instance, module or compatibility domain goes out without its
* optional `id`, since another server rejects ids it did not issue; `call_id` stays, and items
* without a recorded origin keep theirs.
* A function call's `name` is whatever string the model wrote, and one character the endpoint does not
* accept fails the whole request; each such character goes out as `_`. The call still pairs with its
* output through `call_id`.
*/
export function responsesInput(
request: Request,
Expand Down Expand Up @@ -92,6 +101,7 @@ export function responsesInput(
});
const wire = inputItem(entry) as Item;
if (owner && current && !sameService && OPTIONAL_ID_TYPES.has(item.type)) delete wire.id;
if (item.type === 'function_call') wire.name = item.name.replace(INVALID_NAME_CHARS, '_');
const images = imageBlobs(entry.context.blobs);
if (opts.media?.enabled() && images.length && (item.type === 'message' || item.type === 'function_call_output')) {
const field = item.type === 'message' ? 'content' : 'output';
Expand Down
19 changes: 18 additions & 1 deletion tests/providers/transport/responses-input.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Responses 无状态重放的思维链形态。加密形态的规则由 tests/core/session-head.test.ts 覆盖;
* 这里是明文形态:推理项以 reasoning_text 出线,当前轮不受 keepThinking 约束,本地合成的调用前补一项;
* 另有别的端点记下的项出线时 id 的去留。
* 另有别的端点记下的项出线时 id 的去留,以及调用名里端点不收的字符。
*/
import { describe, expect, it } from 'vitest';
import { REASONING_REPLAYS, SYNTHETIC_REASONING_TEXT, responsesInput } from '../../../src/providers/transport/responses-input.ts';
Expand Down Expand Up @@ -134,3 +134,20 @@ describe('responsesInput 跨端点的项 id', () => {
expect(input[3].id).toBe(context[3].item.id);
});
});

describe('responsesInput 历史调用的名字', () => {
it('名字里端点不收的字符换成下划线出线;call_id 和回执不变,记录不改', () => {
const context = [
message('user', 'hi'),
functionCall('call_a', 'app::pet_say', '{"text":"hi"}', { origin: { ...origin, instance: 'other' } }),
functionResult('call_a', '[unknown tool]'),
functionCall('call_b', 'pet.say-2', '{}', { origin }),
functionResult('call_b', 'ok'),
];
const input = replay(context, true, 'encrypted');
expect(input[1]).toMatchObject({ type: 'function_call', name: 'app__pet_say', call_id: 'call_a', arguments: '{"text":"hi"}' });
expect(input[2]).toMatchObject({ type: 'function_call_output', call_id: 'call_a', output: '[unknown tool]' });
expect(input[3]).toMatchObject({ name: 'pet.say-2', call_id: 'call_b' });
expect(context[1].item).toMatchObject({ name: 'app::pet_say' });
});
});
Loading