@oh-my-pi/pi-ai 15.11.1 → 15.11.3

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/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.11.3] - 2026-06-11
6
+ ### Fixed
7
+
8
+ - Fixed GitHub Copilot long-context model requests to use the upstream `requestModelId` when calling Anthropic, OpenAI Responses, and OpenAI Completions APIs
9
+ - Fixed GitHub Copilot model enablement to deduplicate catalog variants by upstream model ID when enabling all models
10
+
11
+ ## [15.11.2] - 2026-06-11
12
+
13
+ ### Fixed
14
+
15
+ - Fixed Anthropic encoding of error tool results with whitespace-only content so requests no longer 400 with `tool_result: content cannot be empty if is_error is true`
16
+
5
17
  ## [15.11.1] - 2026-06-11
6
18
 
7
19
  ### Changed
@@ -3262,4 +3274,4 @@ _Dedicated to Peter's shoulder ([@steipete](https://twitter.com/steipete))_
3262
3274
 
3263
3275
  ## [0.9.4] - 2025-11-26
3264
3276
 
3265
- Initial release with multi-provider LLM support.
3277
+ Initial release with multi-provider LLM support.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "15.11.1",
4
+ "version": "15.11.3",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -38,8 +38,8 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@bufbuild/protobuf": "^2.12.0",
41
- "@oh-my-pi/pi-catalog": "15.11.1",
42
- "@oh-my-pi/pi-utils": "15.11.1",
41
+ "@oh-my-pi/pi-catalog": "15.11.3",
42
+ "@oh-my-pi/pi-utils": "15.11.3",
43
43
  "openai": "^6.39.0",
44
44
  "partial-json": "^0.1.7",
45
45
  "zod": "4.4.3"
@@ -873,13 +873,7 @@ async function prepareAnthropicManyImageContext(context: Context, supportsImages
873
873
  return { ...context, messages };
874
874
  }
875
875
 
876
- /**
877
- * Convert content blocks to Anthropic API format
878
- */
879
- function convertContentBlocks(
880
- content: (TextContent | ImageContent)[],
881
- supportsImages = true,
882
- ):
876
+ type AnthropicToolResultContent =
883
877
  | string
884
878
  | Array<
885
879
  | { type: "text"; text: string }
@@ -891,7 +885,15 @@ function convertContentBlocks(
891
885
  data: string;
892
886
  };
893
887
  }
894
- > {
888
+ >;
889
+
890
+ /**
891
+ * Convert content blocks to Anthropic API format
892
+ */
893
+ function convertContentBlocks(
894
+ content: (TextContent | ImageContent)[],
895
+ supportsImages = true,
896
+ ): AnthropicToolResultContent {
895
897
  const blocks: Array<
896
898
  | { type: "text"; text: string }
897
899
  | {
@@ -2814,7 +2816,7 @@ function buildParams(
2814
2816
  // Build params in the canonical field order: model → messages → system → tools →
2815
2817
  // metadata → max_tokens → thinking → context_management → output_config → stream.
2816
2818
  const params: MessageCreateParamsStreaming = {
2817
- model: model.id,
2819
+ model: model.requestModelId ?? model.id,
2818
2820
  messages: convertAnthropicMessages(context.messages, model, isOAuthToken),
2819
2821
  ...(systemBlocks && { system: systemBlocks }),
2820
2822
  ...(tools !== undefined && { tools }),
@@ -2883,11 +2885,36 @@ function buildParams(
2883
2885
  return params;
2884
2886
  }
2885
2887
 
2888
+ const EMPTY_ERROR_TOOL_RESULT_TEXT = "Tool failed with no output.";
2889
+
2890
+ function isEmptyToolResultWireContent(content: AnthropicToolResultContent): boolean {
2891
+ if (typeof content === "string") {
2892
+ return content.trim().length === 0;
2893
+ }
2894
+ return content.length === 0;
2895
+ }
2896
+
2897
+ function ensureErrorToolResultWireContent(
2898
+ content: AnthropicToolResultContent,
2899
+ isError: boolean | undefined,
2900
+ ): AnthropicToolResultContent {
2901
+ if (!isError || !isEmptyToolResultWireContent(content)) {
2902
+ return content;
2903
+ }
2904
+ return typeof content === "string"
2905
+ ? EMPTY_ERROR_TOOL_RESULT_TEXT
2906
+ : [{ type: "text", text: EMPTY_ERROR_TOOL_RESULT_TEXT }];
2907
+ }
2908
+
2886
2909
  function buildToolResultBlock(model: Model<"anthropic-messages">, msg: ToolResultMessage): ContentBlockParam {
2910
+ const content = ensureErrorToolResultWireContent(
2911
+ convertContentBlocks(msg.content, model.input.includes("image")),
2912
+ msg.isError,
2913
+ );
2887
2914
  const block: ContentBlockParam = {
2888
2915
  type: "tool_result",
2889
2916
  tool_use_id: msg.toolCallId,
2890
- content: convertContentBlocks(msg.content, model.input.includes("image")),
2917
+ content,
2891
2918
  is_error: msg.isError,
2892
2919
  };
2893
2920
  if (model.compat.requiresToolResultId) {
@@ -113,6 +113,8 @@ function resolveOpenAICompletionsModelId(
113
113
  model: Model<"openai-completions">,
114
114
  options: OpenAICompletionsOptions | undefined,
115
115
  ): string {
116
+ // Catalog variants (e.g. Copilot long-context `-1m` entries) pin the wire id.
117
+ if (model.requestModelId) return model.requestModelId;
116
118
  if (model.provider === "firepass") return toFirepassWireModelId(model.id);
117
119
  if (model.provider === "fireworks") return toFireworksWireModelId(model.id);
118
120
  if (model.provider === "openrouter") return applyOpenRouterRoutingVariant(model.id, options?.openrouterVariant);
@@ -460,7 +460,7 @@ function buildParams(
460
460
  const cacheRetention = resolveCacheRetention(options?.cacheRetention);
461
461
  const promptCacheKey = getOpenAIResponsesPromptCacheKey(options);
462
462
  const params: OpenAIResponsesSamplingParams = {
463
- model: model.id,
463
+ model: model.requestModelId ?? model.id,
464
464
  input: messages,
465
465
  instructions: systemInstructions,
466
466
  stream: true,
@@ -4,6 +4,7 @@
4
4
  import { scheduler } from "node:timers/promises";
5
5
  import { getBundledModels } from "@oh-my-pi/pi-catalog/models";
6
6
  import {
7
+ COPILOT_API_HEADERS,
7
8
  getGitHubCopilotBaseUrl,
8
9
  isPublicGitHubHost,
9
10
  normalizeDomain,
@@ -230,7 +231,7 @@ async function enableGitHubCopilotModel(
230
231
  headers: {
231
232
  "Content-Type": "application/json",
232
233
  Authorization: `Bearer ${token}`,
233
- ...OPENCODE_HEADERS,
234
+ ...COPILOT_API_HEADERS,
234
235
  "openai-intent": "chat-policy",
235
236
  "x-interaction-type": "chat-policy",
236
237
  },
@@ -252,14 +253,16 @@ async function enableAllGitHubCopilotModels(
252
253
  fetchImpl: FetchImpl,
253
254
  onProgress?: (model: string, success: boolean) => void,
254
255
  ): Promise<void> {
255
- const models = getBundledModels("github-copilot");
256
+ // Synthesized catalog variants (Copilot long-context `-1m` entries) share
257
+ // the upstream model id; enable each wire id exactly once.
258
+ const wireModelIds = [...new Set(getBundledModels("github-copilot").map(model => model.requestModelId ?? model.id))];
256
259
  const BATCH_SIZE = 5;
257
- for (let i = 0; i < models.length; i += BATCH_SIZE) {
258
- const batch = models.slice(i, i + BATCH_SIZE);
260
+ for (let i = 0; i < wireModelIds.length; i += BATCH_SIZE) {
261
+ const batch = wireModelIds.slice(i, i + BATCH_SIZE);
259
262
  await Promise.all(
260
- batch.map(async model => {
261
- const success = await enableGitHubCopilotModel(token, model.id, fetchImpl, enterpriseDomain);
262
- onProgress?.(model.id, success);
263
+ batch.map(async modelId => {
264
+ const success = await enableGitHubCopilotModel(token, modelId, fetchImpl, enterpriseDomain);
265
+ onProgress?.(modelId, success);
263
266
  }),
264
267
  );
265
268
  }