打通 Boss 直聘落档挂载与真值→投影映射,并修正带 tools 时的推理强度。
落档自动把产物挂进 context_order;创作期用 declare_variable/declare_map 钉目录与分档;真值变更持续重投影。顺带强制 tools 请求 reasoning_effort=none,并支持消息 restart。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+13
-6
@@ -5,6 +5,7 @@
|
||||
|
||||
import {
|
||||
applyRejectedGenerationField,
|
||||
isToolsReasoningEffortConflictError,
|
||||
parseRejectedGenerationField,
|
||||
} from "./generation-compat.js";
|
||||
|
||||
@@ -168,7 +169,7 @@ async function postOnce(
|
||||
|
||||
/**
|
||||
* 探测请求始终带 reasoning_effort=low。
|
||||
* 若供应商拒收该字段,剥掉后再试一次。
|
||||
* 若供应商拒收该字段,剥掉后再试;tools 冲突则改 none 再试。
|
||||
*/
|
||||
async function postChat(
|
||||
config: ProbeConfig,
|
||||
@@ -179,14 +180,20 @@ async function postChat(
|
||||
reasoning_effort: CAPABILITY_PROBE_REASONING_EFFORT,
|
||||
};
|
||||
const first = await postOnce(config, withEffort);
|
||||
if (first.ok || first.status !== 400) return first;
|
||||
|
||||
const rejected = parseRejectedGenerationField(first.text);
|
||||
if (rejected !== "reasoning_effort") return first;
|
||||
if (first.ok || (first.status !== 400 && first.status !== 422)) return first;
|
||||
|
||||
const retryBody = { ...withEffort };
|
||||
const tried = new Set<string>();
|
||||
if (!applyRejectedGenerationField(retryBody, "reasoning_effort", tried)) {
|
||||
|
||||
if (isToolsReasoningEffortConflictError(first.text, first.status)) {
|
||||
retryBody.reasoning_effort = "none";
|
||||
tried.add("reasoning_effort_none");
|
||||
return postOnce(config, retryBody);
|
||||
}
|
||||
|
||||
const rejected = parseRejectedGenerationField(first.text);
|
||||
if (!rejected) return first;
|
||||
if (!applyRejectedGenerationField(retryBody, rejected, tried)) {
|
||||
return first;
|
||||
}
|
||||
return postOnce(config, retryBody);
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { GenerationParameters } from "../types/preset.js";
|
||||
import {
|
||||
applyOpenAiGeneration,
|
||||
fetchWithGenerationCompat,
|
||||
forceReasoningEffortNoneWhenTools,
|
||||
sanitizeReasoningEffort,
|
||||
} from "./generation-compat.js";
|
||||
import { consumeOpenAiToolStream } from "./stream-complete.js";
|
||||
@@ -147,6 +148,8 @@ export function buildRequestBody(
|
||||
applyOpenAiGeneration(body, options?.generation ?? {}, config.model, {
|
||||
reasoningEffort: config.reasoningEffort,
|
||||
});
|
||||
// 对齐 imyai:带 tools 时强制 none(省略字段网关仍可能注入默认 effort)
|
||||
forceReasoningEffortNoneWhenTools(body);
|
||||
|
||||
if (options?.responseFormat === "json_object") {
|
||||
body.response_format = { type: "json_object" };
|
||||
|
||||
@@ -6,7 +6,8 @@ import type { GenerationParameters } from "../types/preset.js";
|
||||
* - top_k、min_p、repetition_penalty 不是官方 Chat Completions 字段,默认不发
|
||||
* - 推理模型去掉 temperature / top_p / penalty
|
||||
* - o 系列与 gpt-5 用 max_completion_tokens
|
||||
* - 仍 400 时按错误信息剥掉不支持字段再试
|
||||
* - 带 tools 时强制 reasoning_effort=none(gpt-5.6-luna 等网关:省略字段仍会注入默认 effort)
|
||||
* - 仍 400 时按错误信息剥掉不支持字段再试;tools+reasoning 冲突则改 none 再试
|
||||
*/
|
||||
|
||||
const REASONING_EFFORT_VALUES = [
|
||||
@@ -157,6 +158,46 @@ export function applyOpenAiGeneration(
|
||||
}
|
||||
}
|
||||
|
||||
/** chat/completions + function tools 时须显式 none,否则网关可能注入默认 effort 并 400。 */
|
||||
export function forceReasoningEffortNoneWhenTools(
|
||||
body: Record<string, unknown>,
|
||||
): void {
|
||||
const tools = body.tools;
|
||||
if (!Array.isArray(tools) || tools.length === 0) return;
|
||||
body.reasoning_effort = "none";
|
||||
}
|
||||
|
||||
/**
|
||||
* gpt-5.6-luna 等:/v1/chat/completions 下 function tools 不能与非 none 的
|
||||
* reasoning_effort 同用。需显式 reasoning_effort='none'。
|
||||
*/
|
||||
export function isToolsReasoningEffortConflictError(
|
||||
errorText: string,
|
||||
status?: number,
|
||||
): boolean {
|
||||
if (status != null && status !== 400 && status !== 422) return false;
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(errorText);
|
||||
} catch {
|
||||
parsed = undefined;
|
||||
}
|
||||
const message = `${readErrorMessage(parsed)}\n${errorText}`.toLowerCase();
|
||||
if (!message.trim()) return false;
|
||||
|
||||
const mentionsTools =
|
||||
/\b(function\s+)?tools?\b/.test(message) ||
|
||||
/tool_choice|function tools/.test(message);
|
||||
const mentionsReasoningEffort = /reasoning_effort/.test(message);
|
||||
const conflict =
|
||||
/not support|unsupported|does not support|not supported|not allowed|cannot|can't/.test(
|
||||
message,
|
||||
);
|
||||
|
||||
return mentionsTools && mentionsReasoningEffort && conflict;
|
||||
}
|
||||
|
||||
function readErrorMessage(raw: unknown): string {
|
||||
if (!raw || typeof raw !== "object") return "";
|
||||
const err = (raw as { error?: unknown }).error;
|
||||
@@ -238,12 +279,42 @@ export function applyRejectedGenerationField(
|
||||
return true;
|
||||
}
|
||||
|
||||
// tools + reasoning_effort 冲突:删字段不够,须显式 none
|
||||
if (
|
||||
field === "reasoning_effort" &&
|
||||
Array.isArray(body.tools) &&
|
||||
body.tools.length > 0 &&
|
||||
body.reasoning_effort !== "none" &&
|
||||
!tried.has("reasoning_effort_none")
|
||||
) {
|
||||
body.reasoning_effort = "none";
|
||||
tried.add("reasoning_effort_none");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(field in body)) return false;
|
||||
delete body[field];
|
||||
tried.add(field);
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyToolsReasoningEffortNone(
|
||||
body: Record<string, unknown>,
|
||||
tried: Set<string>,
|
||||
): boolean {
|
||||
if (
|
||||
!Array.isArray(body.tools) ||
|
||||
body.tools.length === 0 ||
|
||||
body.reasoning_effort === "none" ||
|
||||
tried.has("reasoning_effort_none")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
body.reasoning_effort = "none";
|
||||
tried.add("reasoning_effort_none");
|
||||
return true;
|
||||
}
|
||||
|
||||
function replayResponse(source: Response, text: string): Response {
|
||||
return new Response(text, {
|
||||
status: source.status,
|
||||
@@ -262,11 +333,19 @@ export async function fetchWithGenerationCompat(
|
||||
|
||||
for (let attempt = 0; attempt < 8; attempt++) {
|
||||
last = await send(body);
|
||||
if (last.ok || last.status !== 400) return last;
|
||||
if (last.ok || (last.status !== 400 && last.status !== 422)) return last;
|
||||
const text = await last.text();
|
||||
const next = { ...body };
|
||||
|
||||
if (isToolsReasoningEffortConflictError(text, last.status)) {
|
||||
if (applyToolsReasoningEffortNone(next, tried)) {
|
||||
body = next;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const field = parseRejectedGenerationField(text);
|
||||
if (!field) return replayResponse(last, text);
|
||||
const next = { ...body };
|
||||
if (!applyRejectedGenerationField(next, field, tried)) {
|
||||
return replayResponse(last, text);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user