mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
feat(ai): share runtime schemas with protocol
This commit is contained in:
parent
393c344229
commit
411fe0feae
9 changed files with 180 additions and 124 deletions
1
package-lock.json
generated
1
package-lock.json
generated
|
|
@ -5678,6 +5678,7 @@
|
|||
"version": "0.83.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-ai": "^0.83.0",
|
||||
"typebox": "1.3.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@
|
|||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./schemas": {
|
||||
"types": "./dist/schemas.d.ts",
|
||||
"import": "./dist/schemas.js"
|
||||
},
|
||||
"./compat": {
|
||||
"types": "./dist/compat.d.ts",
|
||||
"import": "./dist/compat.js"
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ export * from "./images-models.ts";
|
|||
export * from "./models.ts";
|
||||
export * from "./models-store.ts";
|
||||
export * from "./providers/faux.ts";
|
||||
export * from "./schemas.ts";
|
||||
export * from "./session-resources.ts";
|
||||
export * from "./types.ts";
|
||||
export * from "./utils/diagnostics.ts";
|
||||
|
|
|
|||
82
packages/ai/src/schemas.ts
Normal file
82
packages/ai/src/schemas.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import Type from "typebox";
|
||||
|
||||
const StrictObject = <T extends Parameters<typeof Type.Object>[0]>(properties: T) =>
|
||||
Type.Object(properties, { additionalProperties: false });
|
||||
|
||||
export const ThinkingLevelSchema = Type.Union([
|
||||
Type.Literal("minimal"),
|
||||
Type.Literal("low"),
|
||||
Type.Literal("medium"),
|
||||
Type.Literal("high"),
|
||||
Type.Literal("xhigh"),
|
||||
Type.Literal("max"),
|
||||
]);
|
||||
|
||||
export const ModelThinkingLevelSchema = Type.Union([Type.Literal("off"), ThinkingLevelSchema]);
|
||||
|
||||
export const TextContentSchema = StrictObject({
|
||||
type: Type.Literal("text"),
|
||||
text: Type.String(),
|
||||
/** Provider message metadata, such as an OpenAI response item identifier. */
|
||||
textSignature: Type.Optional(Type.String()),
|
||||
});
|
||||
|
||||
export const ThinkingContentSchema = StrictObject({
|
||||
type: Type.Literal("thinking"),
|
||||
thinking: Type.String(),
|
||||
/** Opaque provider reasoning identifier or encrypted payload used for multi-turn continuity. */
|
||||
thinkingSignature: Type.Optional(Type.String()),
|
||||
/** Whether safety filters redacted the thinking content. */
|
||||
redacted: Type.Optional(Type.Boolean()),
|
||||
});
|
||||
|
||||
export const ImageContentSchema = StrictObject({
|
||||
type: Type.Literal("image"),
|
||||
/** Base64-encoded image data. */
|
||||
data: Type.String(),
|
||||
mimeType: Type.String({ minLength: 1 }),
|
||||
});
|
||||
|
||||
export const ToolCallSchema = StrictObject({
|
||||
type: Type.Literal("toolCall"),
|
||||
id: Type.String(),
|
||||
name: Type.String(),
|
||||
arguments: Type.Record(Type.String(), Type.Any()),
|
||||
/** Google-specific opaque signature for reusing thought context. */
|
||||
thoughtSignature: Type.Optional(Type.String()),
|
||||
});
|
||||
|
||||
export const UsageSchema = StrictObject({
|
||||
input: Type.Integer({ minimum: 0 }),
|
||||
output: Type.Integer({ minimum: 0 }),
|
||||
cacheRead: Type.Integer({ minimum: 0 }),
|
||||
cacheWrite: Type.Integer({ minimum: 0 }),
|
||||
/** Subset of cacheWrite written with one-hour retention. */
|
||||
cacheWrite1h: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
/** Provider-reported reasoning tokens, already included in output. */
|
||||
reasoning: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
totalTokens: Type.Integer({ minimum: 0 }),
|
||||
cost: StrictObject({
|
||||
input: Type.Number({ minimum: 0 }),
|
||||
output: Type.Number({ minimum: 0 }),
|
||||
cacheRead: Type.Number({ minimum: 0 }),
|
||||
cacheWrite: Type.Number({ minimum: 0 }),
|
||||
total: Type.Number({ minimum: 0 }),
|
||||
}),
|
||||
});
|
||||
|
||||
export const StopReasonSchema = Type.Union([
|
||||
Type.Literal("pending"),
|
||||
Type.Literal("stop"),
|
||||
Type.Literal("length"),
|
||||
Type.Literal("toolUse"),
|
||||
Type.Literal("error"),
|
||||
Type.Literal("aborted"),
|
||||
]);
|
||||
|
||||
export const ModelCostRatesSchema = StrictObject({
|
||||
input: Type.Number({ minimum: 0 }),
|
||||
output: Type.Number({ minimum: 0 }),
|
||||
cacheRead: Type.Number({ minimum: 0 }),
|
||||
cacheWrite: Type.Number({ minimum: 0 }),
|
||||
});
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import type { Static, TSchema } from "typebox";
|
||||
import type { AnthropicOptions } from "./api/anthropic-messages.ts";
|
||||
import type { AzureOpenAIResponsesOptions } from "./api/azure-openai-responses.ts";
|
||||
import type { BedrockOptions } from "./api/bedrock-converse-stream.ts";
|
||||
|
|
@ -8,6 +9,17 @@ import type { OpenAICodexResponsesOptions } from "./api/openai-codex-responses.t
|
|||
import type { OpenAICompletionsOptions } from "./api/openai-completions.ts";
|
||||
import type { OpenAIResponsesOptions } from "./api/openai-responses.ts";
|
||||
import type { PiMessagesOptions } from "./api/pi-messages.ts";
|
||||
import type {
|
||||
ImageContentSchema,
|
||||
ModelCostRatesSchema,
|
||||
ModelThinkingLevelSchema,
|
||||
StopReasonSchema,
|
||||
TextContentSchema,
|
||||
ThinkingContentSchema,
|
||||
ThinkingLevelSchema,
|
||||
ToolCallSchema,
|
||||
UsageSchema,
|
||||
} from "./schemas.ts";
|
||||
import type { AssistantMessageDiagnostic } from "./utils/diagnostics.ts";
|
||||
import type { AssistantMessageEventStream } from "./utils/event-stream.ts";
|
||||
|
||||
|
|
@ -76,8 +88,8 @@ export type KnownImagesProvider = "openrouter";
|
|||
|
||||
export type ImagesProviderId = KnownImagesProvider | string;
|
||||
|
||||
export type ThinkingLevel = "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
||||
export type ModelThinkingLevel = "off" | ThinkingLevel;
|
||||
export type ThinkingLevel = Static<typeof ThinkingLevelSchema>;
|
||||
export type ModelThinkingLevel = Static<typeof ModelThinkingLevelSchema>;
|
||||
export type ThinkingLevelMap = Partial<Record<ModelThinkingLevel, string | null>>;
|
||||
export type ChatTemplateKwargValue =
|
||||
| string
|
||||
|
|
@ -335,60 +347,12 @@ export interface TextSignatureV1 {
|
|||
phase?: "commentary" | "final_answer";
|
||||
}
|
||||
|
||||
export interface TextContent {
|
||||
type: "text";
|
||||
text: string;
|
||||
textSignature?: string; // e.g., for OpenAI responses, message metadata (legacy id string or TextSignatureV1 JSON)
|
||||
}
|
||||
|
||||
export interface ThinkingContent {
|
||||
type: "thinking";
|
||||
thinking: string;
|
||||
thinkingSignature?: string; // e.g., for OpenAI responses, the reasoning item ID
|
||||
/** When true, the thinking content was redacted by safety filters. The opaque
|
||||
* encrypted payload is stored in `thinkingSignature` so it can be passed back
|
||||
* to the API for multi-turn continuity. */
|
||||
redacted?: boolean;
|
||||
}
|
||||
|
||||
export interface ImageContent {
|
||||
type: "image";
|
||||
data: string; // base64 encoded image data
|
||||
mimeType: string; // e.g., "image/jpeg", "image/png"
|
||||
}
|
||||
|
||||
export interface ToolCall {
|
||||
type: "toolCall";
|
||||
id: string;
|
||||
name: string;
|
||||
arguments: Record<string, any>;
|
||||
thoughtSignature?: string; // Google-specific: opaque signature for reusing thought context
|
||||
}
|
||||
|
||||
export interface Usage {
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
/** Subset of `cacheWrite` written with 1h retention. Only Anthropic reports this split. */
|
||||
cacheWrite1h?: number;
|
||||
/**
|
||||
* Reasoning/thinking tokens, when the provider reports them. This is a subset of
|
||||
* `output`: `output` already includes these tokens. Set to a number (possibly 0) by
|
||||
* providers that expose a reasoning breakdown; left undefined by providers that don't.
|
||||
*/
|
||||
reasoning?: number;
|
||||
totalTokens: number;
|
||||
cost: {
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type StopReason = "pending" | "stop" | "length" | "toolUse" | "error" | "aborted";
|
||||
export type TextContent = Static<typeof TextContentSchema>;
|
||||
export type ThinkingContent = Static<typeof ThinkingContentSchema>;
|
||||
export type ImageContent = Static<typeof ImageContentSchema>;
|
||||
export type ToolCall = Static<typeof ToolCallSchema>;
|
||||
export type Usage = Static<typeof UsageSchema>;
|
||||
export type StopReason = Static<typeof StopReasonSchema>;
|
||||
|
||||
export interface UserMessage {
|
||||
role: "user";
|
||||
|
|
@ -453,8 +417,6 @@ export interface AssistantImages {
|
|||
timestamp: number; // Unix timestamp in milliseconds
|
||||
}
|
||||
|
||||
import type { TSchema } from "typebox";
|
||||
|
||||
/** OpenAI grammar variants for constrained sampling. */
|
||||
export type GrammarFormat = "openai_lark" | "openai_regex";
|
||||
|
||||
|
|
@ -738,12 +700,7 @@ export interface VercelGatewayRouting {
|
|||
order?: string[];
|
||||
}
|
||||
|
||||
export interface ModelCostRates {
|
||||
input: number; // $/million tokens
|
||||
output: number; // $/million tokens
|
||||
cacheRead: number; // $/million tokens
|
||||
cacheWrite: number; // $/million tokens
|
||||
}
|
||||
export type ModelCostRates = Static<typeof ModelCostRatesSchema>;
|
||||
|
||||
export interface ModelCostTier extends ModelCostRates {
|
||||
/** Use this tier for requests whose total input usage exceeds this token count. */
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@
|
|||
"directory": "packages/protocol"
|
||||
},
|
||||
"engines": { "node": ">=22.19.0" },
|
||||
"dependencies": { "typebox": "1.3.7" },
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-ai": "^0.83.0",
|
||||
"typebox": "1.3.7"
|
||||
},
|
||||
"devDependencies": { "shx": "0.4.0", "vitest": "4.1.9" }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,13 @@
|
|||
import {
|
||||
ImageContentSchema as AiImageContentSchema,
|
||||
TextContentSchema as AiTextContentSchema,
|
||||
ThinkingContentSchema as AiThinkingContentSchema,
|
||||
UsageSchema as AiUsageSchema,
|
||||
ModelCostRatesSchema,
|
||||
ModelThinkingLevelSchema,
|
||||
StopReasonSchema,
|
||||
ToolCallSchema,
|
||||
} from "@earendil-works/pi-ai/schemas";
|
||||
import Type, { type Static } from "typebox";
|
||||
|
||||
export const PROTOCOL_VERSION = 2 as const;
|
||||
|
|
@ -23,15 +33,7 @@ const JsonValueRecursiveSchema = Type.Cyclic(
|
|||
);
|
||||
export const JsonValueSchema = Type.Unsafe<JsonValue>(JsonValueRecursiveSchema);
|
||||
|
||||
export const ThinkingLevelSchema = Type.Union([
|
||||
Type.Literal("off"),
|
||||
Type.Literal("minimal"),
|
||||
Type.Literal("low"),
|
||||
Type.Literal("medium"),
|
||||
Type.Literal("high"),
|
||||
Type.Literal("xhigh"),
|
||||
Type.Literal("max"),
|
||||
]);
|
||||
export const ThinkingLevelSchema = ModelThinkingLevelSchema;
|
||||
export type ThinkingLevel = Static<typeof ThinkingLevelSchema>;
|
||||
|
||||
/** Matches AgentHarnessPhase so adapters do not need a second phase vocabulary. */
|
||||
|
|
@ -50,12 +52,7 @@ export const ModelRefSchema = StrictObject({
|
|||
});
|
||||
export type ModelRef = Static<typeof ModelRefSchema>;
|
||||
|
||||
export const ModelCostSchema = StrictObject({
|
||||
input: Type.Number({ minimum: 0 }),
|
||||
output: Type.Number({ minimum: 0 }),
|
||||
cacheRead: Type.Number({ minimum: 0 }),
|
||||
cacheWrite: Type.Number({ minimum: 0 }),
|
||||
});
|
||||
export const ModelCostSchema = ModelCostRatesSchema;
|
||||
|
||||
export const ModelMetadataSchema = StrictObject({
|
||||
provider: IdSchema,
|
||||
|
|
@ -72,26 +69,18 @@ export const ModelMetadataSchema = StrictObject({
|
|||
});
|
||||
export type ModelMetadata = Static<typeof ModelMetadataSchema>;
|
||||
|
||||
export const TextContentSchema = StrictObject({
|
||||
type: Type.Literal("text"),
|
||||
text: Type.String(),
|
||||
});
|
||||
export const ThinkingContentSchema = StrictObject({
|
||||
type: Type.Literal("thinking"),
|
||||
thinking: Type.String(),
|
||||
redacted: Type.Optional(Type.Boolean()),
|
||||
});
|
||||
export const ImageContentSchema = StrictObject({
|
||||
type: Type.Literal("image"),
|
||||
data: Type.String(),
|
||||
mimeType: Type.String({ minLength: 1 }),
|
||||
});
|
||||
export const ToolCallContentSchema = StrictObject({
|
||||
type: Type.Literal("tool_call"),
|
||||
toolCallId: IdSchema,
|
||||
toolName: IdSchema,
|
||||
input: JsonValueSchema,
|
||||
});
|
||||
export const TextContentSchema = Type.Omit(AiTextContentSchema, ["textSignature"]);
|
||||
export const ThinkingContentSchema = Type.Omit(AiThinkingContentSchema, ["thinkingSignature"]);
|
||||
export const ImageContentSchema = AiImageContentSchema;
|
||||
export const ToolCallContentSchema = Type.Omit(
|
||||
StrictObject({
|
||||
...ToolCallSchema.properties,
|
||||
id: IdSchema,
|
||||
name: IdSchema,
|
||||
arguments: Type.Record(Type.String(), JsonValueSchema),
|
||||
}),
|
||||
["thoughtSignature"],
|
||||
);
|
||||
export const UserContentSchema = Type.Union([TextContentSchema, ImageContentSchema]);
|
||||
export const AssistantContentSchema = Type.Union([TextContentSchema, ThinkingContentSchema, ToolCallContentSchema]);
|
||||
export const ToolContentSchema = Type.Union([TextContentSchema, ImageContentSchema]);
|
||||
|
|
@ -100,21 +89,7 @@ export type ThinkingContent = Static<typeof ThinkingContentSchema>;
|
|||
export type ImageContent = Static<typeof ImageContentSchema>;
|
||||
export type ToolCallContent = Static<typeof ToolCallContentSchema>;
|
||||
|
||||
export const UsageSchema = StrictObject({
|
||||
input: Type.Integer({ minimum: 0 }),
|
||||
output: Type.Integer({ minimum: 0 }),
|
||||
cacheRead: Type.Integer({ minimum: 0 }),
|
||||
cacheWrite: Type.Integer({ minimum: 0 }),
|
||||
reasoning: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
totalTokens: Type.Integer({ minimum: 0 }),
|
||||
cost: StrictObject({
|
||||
input: Type.Number({ minimum: 0 }),
|
||||
output: Type.Number({ minimum: 0 }),
|
||||
cacheRead: Type.Number({ minimum: 0 }),
|
||||
cacheWrite: Type.Number({ minimum: 0 }),
|
||||
total: Type.Number({ minimum: 0 }),
|
||||
}),
|
||||
});
|
||||
export const UsageSchema = AiUsageSchema;
|
||||
export type Usage = Static<typeof UsageSchema>;
|
||||
|
||||
export const UserTranscriptItemSchema = StrictObject({
|
||||
|
|
@ -136,15 +111,7 @@ export const AssistantTranscriptItemSchema = StrictObject({
|
|||
model: ModelRefSchema,
|
||||
responseModel: Type.Optional(Type.String({ minLength: 1 })),
|
||||
usage: Type.Optional(UsageSchema),
|
||||
stopReason: Type.Optional(
|
||||
Type.Union([
|
||||
Type.Literal("stop"),
|
||||
Type.Literal("length"),
|
||||
Type.Literal("tool_use"),
|
||||
Type.Literal("error"),
|
||||
Type.Literal("aborted"),
|
||||
]),
|
||||
),
|
||||
stopReason: Type.Optional(Type.Exclude(StopReasonSchema, Type.Literal("pending"))),
|
||||
errorMessage: Type.Optional(Type.String()),
|
||||
timestamp: TimestampSchema,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -111,6 +111,15 @@ describe("protocol validation", () => {
|
|||
input: { path: "/tmp/file" },
|
||||
content: [{ type: "text", text: "done" }],
|
||||
details: { lines: [1, 2, 3], cached: false },
|
||||
usage: {
|
||||
input: 1,
|
||||
output: 2,
|
||||
cacheRead: 3,
|
||||
cacheWrite: 4,
|
||||
cacheWrite1h: 2,
|
||||
totalTokens: 10,
|
||||
cost: { input: 0.1, output: 0.2, cacheRead: 0.3, cacheWrite: 0.4, total: 1 },
|
||||
},
|
||||
status: "complete",
|
||||
isError: false,
|
||||
timestamp: 1,
|
||||
|
|
@ -121,6 +130,29 @@ describe("protocol validation", () => {
|
|||
expect(parseServerMessage(message)).toEqual(message);
|
||||
});
|
||||
|
||||
test("uses pi-ai tool calls and stop reasons in transcript items", () => {
|
||||
const message = {
|
||||
type: "event",
|
||||
event: {
|
||||
type: "session_progress",
|
||||
sessionId: "session-1",
|
||||
progress: {
|
||||
type: "item_finished",
|
||||
item: {
|
||||
id: "assistant-1",
|
||||
role: "assistant",
|
||||
content: [{ type: "toolCall", id: "call-1", name: "read", arguments: { path: "/tmp/file" } }],
|
||||
status: "complete",
|
||||
model: { provider: "faux", id: "faux-model" },
|
||||
stopReason: "toolUse",
|
||||
timestamp: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(parseServerMessage(message)).toEqual(message);
|
||||
});
|
||||
|
||||
test("rejects cyclic protocol values with a protocol validation error", () => {
|
||||
const details: Record<string, unknown> = {};
|
||||
details.self = details;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { fileURLToPath } from "node:url";
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
|
|
@ -6,4 +7,12 @@ export default defineConfig({
|
|||
environment: "node",
|
||||
reporters: process.env.GITHUB_ACTIONS ? ["dot", "github-actions"] : ["dot"],
|
||||
},
|
||||
resolve: {
|
||||
alias: [
|
||||
{
|
||||
find: /^@earendil-works\/pi-ai\/schemas$/,
|
||||
replacement: fileURLToPath(new URL("../ai/src/schemas.ts", import.meta.url)),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue