mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
feat(ai): add simple tool choice option
This commit is contained in:
parent
ad58801ce7
commit
e5dde9a76b
14 changed files with 74 additions and 15 deletions
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
### Added
|
||||
|
||||
- Added provider-neutral `toolChoice` support to simple stream requests.
|
||||
- Added China-specific ZAI Coding Plan models, including GLM-4.6V vision support, and API-equivalent usage cost estimates for models with published PAYG prices ([#8220](https://github.com/earendil-works/pi/issues/8220)).
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -834,7 +834,10 @@ export const streamSimple: StreamFunction<"anthropic-messages", SimpleStreamOpti
|
|||
): AssistantMessageEventStream => {
|
||||
assertRequestAuth(model.provider, options?.apiKey, options?.headers);
|
||||
|
||||
const base = buildBaseOptions(model, context, options, options?.apiKey);
|
||||
const base = {
|
||||
...buildBaseOptions(model, context, options, options?.apiKey),
|
||||
toolChoice: options?.toolChoice,
|
||||
} satisfies AnthropicOptions;
|
||||
if (!options?.reasoning) {
|
||||
return stream(model, context, {
|
||||
...base,
|
||||
|
|
|
|||
|
|
@ -169,7 +169,10 @@ export const streamSimple: StreamFunction<"azure-openai-responses", SimpleStream
|
|||
throw new Error(`No API key for provider: ${model.provider}`);
|
||||
}
|
||||
|
||||
const base = buildBaseOptions(model, context, options, apiKey);
|
||||
const base = {
|
||||
...buildBaseOptions(model, context, options, apiKey),
|
||||
toolChoice: options?.toolChoice,
|
||||
} satisfies AzureOpenAIResponsesOptions;
|
||||
const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;
|
||||
const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning;
|
||||
|
||||
|
|
|
|||
|
|
@ -505,7 +505,10 @@ export const streamSimple: StreamFunction<"bedrock-converse-stream", SimpleStrea
|
|||
context: Context,
|
||||
options?: SimpleStreamOptions,
|
||||
): AssistantMessageEventStream => {
|
||||
const base = buildBaseOptions(model, context, options, undefined);
|
||||
const base = {
|
||||
...buildBaseOptions(model, context, options, undefined),
|
||||
toolChoice: options?.toolChoice,
|
||||
} satisfies BedrockOptions;
|
||||
if (!options?.reasoning) {
|
||||
return stream(model, context, { ...base, reasoning: undefined } satisfies BedrockOptions);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -303,7 +303,10 @@ export const streamSimple: StreamFunction<"google-generative-ai", SimpleStreamOp
|
|||
throw new Error(`No API key for provider: ${model.provider}`);
|
||||
}
|
||||
|
||||
const base = buildBaseOptions(model, context, options, apiKey);
|
||||
const base = {
|
||||
...buildBaseOptions(model, context, options, apiKey),
|
||||
toolChoice: options?.toolChoice,
|
||||
} satisfies GoogleOptions;
|
||||
if (!options?.reasoning) {
|
||||
return stream(model, context, { ...base, thinking: { enabled: false } } satisfies GoogleOptions);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -315,7 +315,10 @@ export const streamSimple: StreamFunction<"google-vertex", SimpleStreamOptions>
|
|||
context: Context,
|
||||
options?: SimpleStreamOptions,
|
||||
): AssistantMessageEventStream => {
|
||||
const base = buildBaseOptions(model, context, options, undefined);
|
||||
const base = {
|
||||
...buildBaseOptions(model, context, options, undefined),
|
||||
toolChoice: options?.toolChoice,
|
||||
} satisfies GoogleVertexOptions;
|
||||
if (!options?.reasoning) {
|
||||
return stream(model, context, {
|
||||
...base,
|
||||
|
|
|
|||
|
|
@ -187,7 +187,10 @@ export const streamSimple: StreamFunction<"mistral-conversations", SimpleStreamO
|
|||
throw new Error(`No API key for provider: ${model.provider}`);
|
||||
}
|
||||
|
||||
const base = buildBaseOptions(model, context, options, apiKey);
|
||||
const base = {
|
||||
...buildBaseOptions(model, context, options, apiKey),
|
||||
toolChoice: options?.toolChoice,
|
||||
} satisfies MistralOptions;
|
||||
const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;
|
||||
const reasoning = clampedReasoning === "off" ? undefined : clampedReasoning;
|
||||
const shouldUseReasoning = model.reasoning && reasoning !== undefined;
|
||||
|
|
|
|||
|
|
@ -499,7 +499,10 @@ export const streamSimple: StreamFunction<"openai-codex-responses", SimpleStream
|
|||
throw new Error(`No API key for provider: ${model.provider}`);
|
||||
}
|
||||
|
||||
const base = buildBaseOptions(model, context, options, apiKey);
|
||||
const base = {
|
||||
...buildBaseOptions(model, context, options, apiKey),
|
||||
toolChoice: options?.toolChoice,
|
||||
} satisfies OpenAICodexResponsesOptions;
|
||||
const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;
|
||||
const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning;
|
||||
|
||||
|
|
|
|||
|
|
@ -622,15 +622,16 @@ export const streamSimple: StreamFunction<"openai-completions", SimpleStreamOpti
|
|||
): AssistantMessageEventStream => {
|
||||
getClientApiKey(model.provider, options?.apiKey, options?.headers);
|
||||
|
||||
const base = buildBaseOptions(model, context, options, options?.apiKey);
|
||||
const base = {
|
||||
...buildBaseOptions(model, context, options, options?.apiKey),
|
||||
toolChoice: options?.toolChoice,
|
||||
} satisfies OpenAICompletionsOptions;
|
||||
const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;
|
||||
const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning;
|
||||
const toolChoice = (options as OpenAICompletionsOptions | undefined)?.toolChoice;
|
||||
|
||||
return stream(model, context, {
|
||||
...base,
|
||||
reasoningEffort,
|
||||
toolChoice,
|
||||
thinkingBudgets: options?.thinkingBudgets,
|
||||
} satisfies OpenAICompletionsOptions);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -202,7 +202,10 @@ export const streamSimple: StreamFunction<"openai-responses", SimpleStreamOption
|
|||
): AssistantMessageEventStream => {
|
||||
getClientApiKey(model.provider, options?.apiKey, options?.headers);
|
||||
|
||||
const base = buildBaseOptions(model, context, options, options?.apiKey);
|
||||
const base = {
|
||||
...buildBaseOptions(model, context, options, options?.apiKey),
|
||||
toolChoice: options?.toolChoice,
|
||||
} satisfies OpenAIResponsesOptions;
|
||||
const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;
|
||||
const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning;
|
||||
|
||||
|
|
|
|||
|
|
@ -427,7 +427,7 @@ export const streamSimple: StreamFunction<"pi-messages", SimpleStreamOptions> =
|
|||
return stream(model, context, {
|
||||
...options,
|
||||
reasoning: options?.reasoning,
|
||||
toolChoice: extra?.toolChoice,
|
||||
toolChoice: options?.toolChoice,
|
||||
debug: extra?.debug,
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ export type KnownImagesProvider = "openrouter";
|
|||
|
||||
export type ImagesProviderId = KnownImagesProvider | string;
|
||||
|
||||
export type ToolChoice = "auto" | "none";
|
||||
export type ThinkingLevel = "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
||||
export type ModelThinkingLevel = "off" | ThinkingLevel;
|
||||
export type ThinkingLevelMap = Partial<Record<ModelThinkingLevel, string | null>>;
|
||||
|
|
@ -304,6 +305,8 @@ export type AnthropicRefusalFallback = "default" | readonly { model: string }[];
|
|||
|
||||
// Unified options with reasoning passed to streamSimple() and completeSimple()
|
||||
export interface SimpleStreamOptions extends StreamOptions {
|
||||
/** Provider-neutral tool selection for simple requests. Default: "auto". */
|
||||
toolChoice?: ToolChoice;
|
||||
reasoning?: ThinkingLevel;
|
||||
/** Anthropic server-side fallback for eligible refusal stop reasons. Anthropic providers only. */
|
||||
refusalFallbacks?: AnthropicRefusalFallback;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Type } from "typebox";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { stream } from "../src/api/azure-openai-responses.ts";
|
||||
import { stream, streamSimple } from "../src/api/azure-openai-responses.ts";
|
||||
import type { Model } from "../src/types.ts";
|
||||
|
||||
const model: Model<"azure-openai-responses"> = {
|
||||
|
|
@ -46,4 +46,34 @@ describe("Azure OpenAI tool choice", () => {
|
|||
expect(payload).toMatchObject({ tool_choice: "required" });
|
||||
expect((payload as { tools?: unknown[] }).tools).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("forwards provider-neutral tool choice from simple options", async () => {
|
||||
let payload: unknown;
|
||||
const result = streamSimple(
|
||||
model,
|
||||
{
|
||||
messages: [{ role: "user", content: "Summarize this", timestamp: 1 }],
|
||||
tools: [
|
||||
{
|
||||
name: "read",
|
||||
description: "Read a file",
|
||||
parameters: Type.Object({ path: Type.String() }),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
apiKey: "test-key",
|
||||
toolChoice: "none",
|
||||
onPayload: (requestPayload) => {
|
||||
payload = requestPayload;
|
||||
throw new Error("payload captured");
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await result.result();
|
||||
|
||||
expect(payload).toMatchObject({ tool_choice: "none" });
|
||||
expect((payload as { tools?: unknown[] }).tools).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -165,13 +165,13 @@ describe("pi-messages", () => {
|
|||
const model = createModel(baseUrl);
|
||||
|
||||
let observedHeaders: Record<string, string> | undefined;
|
||||
const options: PiMessagesOptions = {
|
||||
const options = {
|
||||
apiKey: "test-key",
|
||||
debug: true,
|
||||
onResponse: (response) => {
|
||||
observedHeaders = response.headers;
|
||||
},
|
||||
};
|
||||
} satisfies PiMessagesOptions;
|
||||
const message = await streamSimple(model, context, options).result();
|
||||
|
||||
expect(message.stopReason).toBe("stop");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue