@oh-my-pi/pi-ai 12.19.2 → 13.0.0
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/package.json +2 -2
- package/src/providers/amazon-bedrock.ts +1 -0
- package/src/providers/anthropic.ts +1 -1
- package/src/providers/azure-openai-responses.ts +37 -0
- package/src/providers/cursor.ts +6 -6
- package/src/providers/google-gemini-cli.ts +1 -1
- package/src/providers/google-shared.ts +1 -1
- package/src/providers/openai-codex-responses.ts +36 -0
- package/src/providers/openai-completions.ts +16 -5
- package/src/providers/openai-responses.ts +1 -1
- package/src/providers/transform-messages.ts +11 -12
- package/src/types.ts +7 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "13.0.0",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"@connectrpc/connect-node": "^2.1",
|
|
45
45
|
"@google/genai": "^1.42",
|
|
46
46
|
"@mistralai/mistralai": "^1.14",
|
|
47
|
-
"@oh-my-pi/pi-utils": "
|
|
47
|
+
"@oh-my-pi/pi-utils": "13.0.0",
|
|
48
48
|
"@sinclair/typebox": "^0.34",
|
|
49
49
|
"@smithy/node-http-handler": "^4.4",
|
|
50
50
|
"ajv": "^8.18",
|
|
@@ -967,7 +967,7 @@ export function convertAnthropicMessages(
|
|
|
967
967
|
for (let i = 0; i < transformedMessages.length; i++) {
|
|
968
968
|
const msg = transformedMessages[i];
|
|
969
969
|
|
|
970
|
-
if (msg.role === "user") {
|
|
970
|
+
if (msg.role === "user" || msg.role === "developer") {
|
|
971
971
|
if (!msg.content) continue;
|
|
972
972
|
|
|
973
973
|
if (typeof msg.content === "string") {
|
|
@@ -575,6 +575,43 @@ function convertMessages(
|
|
|
575
575
|
content: filteredContent,
|
|
576
576
|
});
|
|
577
577
|
}
|
|
578
|
+
} else if (msg.role === "developer") {
|
|
579
|
+
const devRole = "user";
|
|
580
|
+
if (typeof msg.content === "string") {
|
|
581
|
+
if (!msg.content || msg.content.trim() === "") continue;
|
|
582
|
+
messages.push({
|
|
583
|
+
role: devRole,
|
|
584
|
+
content: sanitizeSurrogates(msg.content),
|
|
585
|
+
});
|
|
586
|
+
} else {
|
|
587
|
+
const content: ResponseInputContent[] = msg.content.map((item): ResponseInputContent => {
|
|
588
|
+
if (item.type === "text") {
|
|
589
|
+
return {
|
|
590
|
+
type: "input_text",
|
|
591
|
+
text: sanitizeSurrogates(item.text),
|
|
592
|
+
} satisfies ResponseInputText;
|
|
593
|
+
}
|
|
594
|
+
return {
|
|
595
|
+
type: "input_image",
|
|
596
|
+
detail: "auto",
|
|
597
|
+
image_url: `data:${item.mimeType};base64,${item.data}`,
|
|
598
|
+
} satisfies ResponseInputImage;
|
|
599
|
+
});
|
|
600
|
+
let filteredContent = !model.input.includes("image")
|
|
601
|
+
? content.filter(c => c.type !== "input_image")
|
|
602
|
+
: content;
|
|
603
|
+
filteredContent = filteredContent.filter(c => {
|
|
604
|
+
if (c.type === "input_text") {
|
|
605
|
+
return c.text.trim().length > 0;
|
|
606
|
+
}
|
|
607
|
+
return true;
|
|
608
|
+
});
|
|
609
|
+
if (filteredContent.length === 0) continue;
|
|
610
|
+
messages.push({
|
|
611
|
+
role: devRole,
|
|
612
|
+
content: filteredContent,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
578
615
|
} else if (msg.role === "assistant") {
|
|
579
616
|
const output: ResponseInput = [];
|
|
580
617
|
const assistantMsg = msg as AssistantMessage;
|
package/src/providers/cursor.ts
CHANGED
|
@@ -1834,10 +1834,10 @@ function buildMcpToolDefinitions(tools: Tool[] | undefined): McpToolDefinition[]
|
|
|
1834
1834
|
}
|
|
1835
1835
|
|
|
1836
1836
|
/**
|
|
1837
|
-
* Extract text content from a user message.
|
|
1837
|
+
* Extract text content from a user or developer message.
|
|
1838
1838
|
*/
|
|
1839
1839
|
function extractUserMessageText(msg: Message): string {
|
|
1840
|
-
if (msg.role !== "user") return "";
|
|
1840
|
+
if (msg.role !== "user" && msg.role !== "developer") return "";
|
|
1841
1841
|
const content = msg.content;
|
|
1842
1842
|
if (typeof content === "string") return content.trim();
|
|
1843
1843
|
const text = content
|
|
@@ -1874,7 +1874,7 @@ function buildConversationTurns(messages: Message[]): Uint8Array[] {
|
|
|
1874
1874
|
const msg = messages[i];
|
|
1875
1875
|
|
|
1876
1876
|
// Skip non-user messages at the start
|
|
1877
|
-
if (msg.role !== "user") {
|
|
1877
|
+
if (msg.role !== "user" && msg.role !== "developer") {
|
|
1878
1878
|
i++;
|
|
1879
1879
|
continue;
|
|
1880
1880
|
}
|
|
@@ -1882,7 +1882,7 @@ function buildConversationTurns(messages: Message[]): Uint8Array[] {
|
|
|
1882
1882
|
// Check if this is the last user message (which goes in the action, not turns)
|
|
1883
1883
|
let isLastUserMessage = true;
|
|
1884
1884
|
for (let j = i + 1; j < messages.length; j++) {
|
|
1885
|
-
if (messages[j].role === "user") {
|
|
1885
|
+
if (messages[j].role === "user" || messages[j].role === "developer") {
|
|
1886
1886
|
isLastUserMessage = false;
|
|
1887
1887
|
break;
|
|
1888
1888
|
}
|
|
@@ -1908,7 +1908,7 @@ function buildConversationTurns(messages: Message[]): Uint8Array[] {
|
|
|
1908
1908
|
const stepBytes: Uint8Array[] = [];
|
|
1909
1909
|
i++;
|
|
1910
1910
|
|
|
1911
|
-
while (i < messages.length && messages[i].role !== "user") {
|
|
1911
|
+
while (i < messages.length && messages[i].role !== "user" && messages[i].role !== "developer") {
|
|
1912
1912
|
const stepMsg = messages[i];
|
|
1913
1913
|
|
|
1914
1914
|
if (stepMsg.role === "assistant") {
|
|
@@ -1982,7 +1982,7 @@ function buildGrpcRequest(
|
|
|
1982
1982
|
|
|
1983
1983
|
const lastMessage = context.messages[context.messages.length - 1];
|
|
1984
1984
|
const userText =
|
|
1985
|
-
lastMessage?.role === "user"
|
|
1985
|
+
lastMessage?.role === "user" || lastMessage?.role === "developer"
|
|
1986
1986
|
? typeof lastMessage.content === "string"
|
|
1987
1987
|
? lastMessage.content.trim()
|
|
1988
1988
|
: extractText(lastMessage.content)
|
|
@@ -788,7 +788,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
788
788
|
|
|
789
789
|
function deriveSessionId(context: Context): string | undefined {
|
|
790
790
|
for (const message of context.messages) {
|
|
791
|
-
if (message.role !== "user") {
|
|
791
|
+
if (message.role !== "user" && message.role !== "developer") {
|
|
792
792
|
continue;
|
|
793
793
|
}
|
|
794
794
|
|
|
@@ -83,7 +83,7 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
|
|
|
83
83
|
const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);
|
|
84
84
|
|
|
85
85
|
for (const msg of transformedMessages) {
|
|
86
|
-
if (msg.role === "user") {
|
|
86
|
+
if (msg.role === "user" || msg.role === "developer") {
|
|
87
87
|
if (typeof msg.content === "string") {
|
|
88
88
|
// Skip empty user messages
|
|
89
89
|
if (!msg.content || msg.content.trim() === "") continue;
|
|
@@ -1563,6 +1563,42 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
|
|
|
1563
1563
|
content: filteredContent,
|
|
1564
1564
|
});
|
|
1565
1565
|
}
|
|
1566
|
+
} else if (msg.role === "developer") {
|
|
1567
|
+
if (typeof msg.content === "string") {
|
|
1568
|
+
if (!msg.content || msg.content.trim() === "") continue;
|
|
1569
|
+
messages.push({
|
|
1570
|
+
role: "developer",
|
|
1571
|
+
content: [{ type: "input_text", text: sanitizeSurrogates(msg.content) }],
|
|
1572
|
+
});
|
|
1573
|
+
} else {
|
|
1574
|
+
const content: ResponseInputContent[] = msg.content.map((item): ResponseInputContent => {
|
|
1575
|
+
if (item.type === "text") {
|
|
1576
|
+
return {
|
|
1577
|
+
type: "input_text",
|
|
1578
|
+
text: sanitizeSurrogates(item.text),
|
|
1579
|
+
} satisfies ResponseInputText;
|
|
1580
|
+
}
|
|
1581
|
+
return {
|
|
1582
|
+
type: "input_image",
|
|
1583
|
+
detail: "auto",
|
|
1584
|
+
image_url: `data:${item.mimeType};base64,${item.data}`,
|
|
1585
|
+
} satisfies ResponseInputImage;
|
|
1586
|
+
});
|
|
1587
|
+
let filteredContent = !model.input.includes("image")
|
|
1588
|
+
? content.filter(c => c.type !== "input_image")
|
|
1589
|
+
: content;
|
|
1590
|
+
filteredContent = filteredContent.filter(c => {
|
|
1591
|
+
if (c.type === "input_text") {
|
|
1592
|
+
return c.text.trim().length > 0;
|
|
1593
|
+
}
|
|
1594
|
+
return true;
|
|
1595
|
+
});
|
|
1596
|
+
if (filteredContent.length === 0) continue;
|
|
1597
|
+
messages.push({
|
|
1598
|
+
role: "developer",
|
|
1599
|
+
content: filteredContent,
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1566
1602
|
} else if (msg.role === "assistant") {
|
|
1567
1603
|
const output: ResponseInput = [];
|
|
1568
1604
|
|
|
@@ -583,7 +583,7 @@ function maybeAddOpenRouterAnthropicCacheControl(
|
|
|
583
583
|
// on the last user/assistant message (walking backwards until we find text content).
|
|
584
584
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
585
585
|
const msg = messages[i];
|
|
586
|
-
if (msg.role !== "user" && msg.role !== "assistant") continue;
|
|
586
|
+
if (msg.role !== "user" && msg.role !== "assistant" && msg.role !== "developer") continue;
|
|
587
587
|
|
|
588
588
|
const content = msg.content;
|
|
589
589
|
if (typeof content === "string") {
|
|
@@ -643,19 +643,25 @@ export function convertMessages(
|
|
|
643
643
|
const msg = transformedMessages[i];
|
|
644
644
|
// Some providers (e.g. Mistral/Devstral) don't allow user messages directly after tool results
|
|
645
645
|
// Insert a synthetic assistant message to bridge the gap
|
|
646
|
-
if (
|
|
646
|
+
if (
|
|
647
|
+
compat.requiresAssistantAfterToolResult &&
|
|
648
|
+
lastRole === "toolResult" &&
|
|
649
|
+
(msg.role === "user" || msg.role === "developer")
|
|
650
|
+
) {
|
|
647
651
|
params.push({
|
|
648
652
|
role: "assistant",
|
|
649
653
|
content: "I have processed the tool results.",
|
|
650
654
|
});
|
|
651
655
|
}
|
|
652
656
|
|
|
653
|
-
|
|
657
|
+
const devAsUser = !compat.supportsDeveloperRole;
|
|
658
|
+
if (msg.role === "user" || msg.role === "developer") {
|
|
659
|
+
const role = !devAsUser && msg.role === "developer" ? "developer" : "user";
|
|
654
660
|
if (typeof msg.content === "string") {
|
|
655
661
|
const text = sanitizeSurrogates(msg.content);
|
|
656
662
|
if (text.trim().length === 0) continue;
|
|
657
663
|
params.push({
|
|
658
|
-
role:
|
|
664
|
+
role: role,
|
|
659
665
|
content: text,
|
|
660
666
|
});
|
|
661
667
|
} else {
|
|
@@ -870,7 +876,12 @@ export function convertMessages(
|
|
|
870
876
|
continue;
|
|
871
877
|
}
|
|
872
878
|
|
|
873
|
-
lastRole =
|
|
879
|
+
lastRole =
|
|
880
|
+
msg.role === "developer"
|
|
881
|
+
? model.reasoning && compat.supportsDeveloperRole
|
|
882
|
+
? "developer"
|
|
883
|
+
: "system"
|
|
884
|
+
: msg.role;
|
|
874
885
|
}
|
|
875
886
|
|
|
876
887
|
return params;
|
|
@@ -505,7 +505,7 @@ function convertMessages(
|
|
|
505
505
|
|
|
506
506
|
let msgIndex = 0;
|
|
507
507
|
for (const msg of transformedMessages) {
|
|
508
|
-
if (msg.role === "user") {
|
|
508
|
+
if (msg.role === "user" || msg.role === "developer") {
|
|
509
509
|
if (typeof msg.content === "string") {
|
|
510
510
|
// Skip empty user messages
|
|
511
511
|
if (!msg.content || msg.content.trim() === "") continue;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { Api, AssistantMessage, Message, Model, ToolCall, ToolResultMessage
|
|
1
|
+
import type { Api, AssistantMessage, DeveloperMessage, Message, Model, ToolCall, ToolResultMessage } from "../types";
|
|
2
2
|
|
|
3
3
|
const TURN_ABORTED_GUIDANCE =
|
|
4
|
-
"<
|
|
4
|
+
"<turn-aborted>\n" +
|
|
5
5
|
"The previous turn was aborted. Any running tools/commands were terminated. " +
|
|
6
6
|
"If tools were aborted, they may have partially executed; verify current state before retrying.\n" +
|
|
7
|
-
"</
|
|
7
|
+
"</turn-aborted>";
|
|
8
8
|
|
|
9
9
|
const enum ToolCallStatus {
|
|
10
10
|
/** Tool call has received a result (real or synthetic for orphan) */
|
|
@@ -21,7 +21,7 @@ const enum ToolCallStatus {
|
|
|
21
21
|
* For aborted/errored turns, this function:
|
|
22
22
|
* - Preserves tool call structure (unlike converting to text summaries)
|
|
23
23
|
* - Injects synthetic "aborted" tool results
|
|
24
|
-
* - Adds a <
|
|
24
|
+
* - Adds a <turn-aborted> guidance marker for the model
|
|
25
25
|
*/
|
|
26
26
|
export function transformMessages<TApi extends Api>(
|
|
27
27
|
messages: Message[],
|
|
@@ -33,8 +33,8 @@ export function transformMessages<TApi extends Api>(
|
|
|
33
33
|
|
|
34
34
|
// First pass: transform messages (thinking blocks, tool call ID normalization)
|
|
35
35
|
const transformed = messages.map(msg => {
|
|
36
|
-
// User messages pass through unchanged
|
|
37
|
-
if (msg.role === "user") {
|
|
36
|
+
// User and developer messages pass through unchanged
|
|
37
|
+
if (msg.role === "user" || msg.role === "developer") {
|
|
38
38
|
return msg;
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -160,13 +160,12 @@ export function transformMessages<TApi extends Api>(
|
|
|
160
160
|
} as ToolResultMessage);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
// Inject turn_aborted guidance marker as
|
|
163
|
+
// Inject turn_aborted guidance marker as developer message
|
|
164
164
|
result.push({
|
|
165
|
-
role: "
|
|
165
|
+
role: "developer",
|
|
166
166
|
content: TURN_ABORTED_GUIDANCE,
|
|
167
|
-
synthetic: true,
|
|
168
167
|
timestamp: assistantMsg.timestamp + 1,
|
|
169
|
-
} as
|
|
168
|
+
} as DeveloperMessage);
|
|
170
169
|
|
|
171
170
|
continue;
|
|
172
171
|
}
|
|
@@ -182,8 +181,8 @@ export function transformMessages<TApi extends Api>(
|
|
|
182
181
|
if (toolCallStatus.get(msg.toolCallId) === ToolCallStatus.Aborted) continue;
|
|
183
182
|
toolCallStatus.set(msg.toolCallId, ToolCallStatus.Resolved);
|
|
184
183
|
result.push(msg);
|
|
185
|
-
} else if (msg.role === "user") {
|
|
186
|
-
// User message interrupts tool flow - insert synthetic results for orphaned calls
|
|
184
|
+
} else if (msg.role === "user" || msg.role === "developer") {
|
|
185
|
+
// User/developer message interrupts tool flow - insert synthetic results for orphaned calls
|
|
187
186
|
if (pendingToolCalls.length > 0) {
|
|
188
187
|
for (const tc of pendingToolCalls) {
|
|
189
188
|
if (!toolCallStatus.has(tc.id)) {
|
package/src/types.ts
CHANGED
|
@@ -254,6 +254,12 @@ export interface UserMessage {
|
|
|
254
254
|
timestamp: number; // Unix timestamp in milliseconds
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
export interface DeveloperMessage {
|
|
258
|
+
role: "developer";
|
|
259
|
+
content: string | (TextContent | ImageContent)[];
|
|
260
|
+
timestamp: number; // Unix timestamp in milliseconds
|
|
261
|
+
}
|
|
262
|
+
|
|
257
263
|
export interface AssistantMessage {
|
|
258
264
|
role: "assistant";
|
|
259
265
|
content: (TextContent | ThinkingContent | ToolCall)[];
|
|
@@ -281,7 +287,7 @@ export interface ToolResultMessage<TDetails = any, TInput = unknown> {
|
|
|
281
287
|
$normative?: TInput;
|
|
282
288
|
}
|
|
283
289
|
|
|
284
|
-
export type Message = UserMessage | AssistantMessage | ToolResultMessage;
|
|
290
|
+
export type Message = UserMessage | DeveloperMessage | AssistantMessage | ToolResultMessage;
|
|
285
291
|
|
|
286
292
|
export type CursorExecHandlerResult<T> = { result: T; toolResult?: ToolResultMessage } | T | ToolResultMessage;
|
|
287
293
|
|