@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "12.19.2",
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": "12.19.2",
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",
@@ -387,6 +387,7 @@ function convertMessages(
387
387
  const m = transformedMessages[i];
388
388
 
389
389
  switch (m.role) {
390
+ case "developer":
390
391
  case "user":
391
392
  if (typeof m.content === "string") {
392
393
  // Skip empty user messages
@@ -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;
@@ -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 (compat.requiresAssistantAfterToolResult && lastRole === "toolResult" && msg.role === "user") {
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
- if (msg.role === "user") {
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: "user",
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 = msg.role;
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, UserMessage } from "../types";
1
+ import type { Api, AssistantMessage, DeveloperMessage, Message, Model, ToolCall, ToolResultMessage } from "../types";
2
2
 
3
3
  const TURN_ABORTED_GUIDANCE =
4
- "<turn_aborted>\n" +
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
- "</turn_aborted>";
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 <turn_aborted> guidance marker for the model
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 synthetic user message
163
+ // Inject turn_aborted guidance marker as developer message
164
164
  result.push({
165
- role: "user",
165
+ role: "developer",
166
166
  content: TURN_ABORTED_GUIDANCE,
167
- synthetic: true,
168
167
  timestamp: assistantMsg.timestamp + 1,
169
- } as UserMessage);
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