@computesdk/pi-morph 1.0.3 → 1.0.4

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/index.ts CHANGED
@@ -6,8 +6,15 @@
6
6
  // built-in `openai-completions` implementation handles the request/stream/tool-call plumbing. Load it with
7
7
  // `pi -e ./index.ts` or install the package (`pi install npm:@computesdk/pi-morph`).
8
8
  //
9
+ // It uses the name+config form of `pi.registerProvider` on purpose: building a Provider with
10
+ // `createProvider` needs a real API implementation, and the only export of one lives at the
11
+ // `@earendil-works/pi-ai/api/openai-completions.lazy` subpath. Pi maps bare `@earendil-works/*` imports
12
+ // to its own bundled copies but does not map subpaths, so that import resolves to the package's own
13
+ // node_modules copy, which then fails at runtime under pnpm (its transitive deps are invisible to Pi's
14
+ // bundled resolver). The config form names the built-in API by string and never imports pi-ai at runtime.
15
+ //
9
16
  // Only the general chat models are registered. Morph's single-purpose models — Fast Apply (`morph-v3-fast`,
10
- // `morph-v3-large`, `auto`), WarpGrep (`morph-warp-grep-v2.1`), Compact and the Router — take bespoke request
17
+ // `morph-v2`, `morph-v0`), WarpGrep (`morph-warp-grep-v2.1`), Compact and the Router — take bespoke request
11
18
  // shapes and are not chat models, so they are left out. `service_tier: "standby"` halves the price on
12
19
  // supported models; Pi can send it via `samplingParams` if you want it on.
13
20
  //
@@ -15,8 +22,7 @@
15
22
  // there when they change.
16
23
 
17
24
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
18
- import { createProvider, envApiKeyAuth, type Model, type ThinkingLevelMap } from "@earendil-works/pi-ai";
19
- import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
25
+ import type { Model, ThinkingLevelMap } from "@earendil-works/pi-ai";
20
26
 
21
27
  const ID = "morph";
22
28
  const BASE_URL = "https://api.morphllm.com/v1";
@@ -61,17 +67,14 @@ export const MODELS: ChatModel[] = [
61
67
  ];
62
68
 
63
69
  export default function morphProvider(pi: ExtensionAPI) {
64
- // A complete provider rather than the name+config form, because that is what carries native
65
- // auth: envApiKeyAuth gives `/login morph` a key prompt, stores the credential in auth.json,
66
- // and falls back to MORPH_API_KEY in the environment when nothing is stored.
67
- pi.registerProvider(
68
- createProvider({
69
- id: ID,
70
- name: "Morph",
71
- baseUrl: BASE_URL,
72
- auth: { apiKey: envApiKeyAuth("Morph API key", ["MORPH_API_KEY"]) },
73
- models: MODELS,
74
- api: openAICompletionsApi(),
75
- }),
76
- );
70
+ // Auth: the key `/login morph` stores in auth.json, or MORPH_API_KEY in the environment
71
+ // via the `$` interpolation (used when nothing is stored).
72
+ pi.registerProvider(ID, {
73
+ name: "Morph",
74
+ baseUrl: BASE_URL,
75
+ apiKey: "$MORPH_API_KEY",
76
+ api: "openai-completions",
77
+ authHeader: true,
78
+ models: MODELS,
79
+ });
77
80
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@computesdk/pi-morph",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Morph for Pi: open-weight coding models (Kimi K3, GLM 5.3, DeepSeek V4) on Morph's OpenAI-compatible API, plus Fast Apply, Compact, and WarpGrep as tools.",
5
5
  "keywords": [
6
6
  "pi-package",
package/tools/apply.ts CHANGED
@@ -4,8 +4,12 @@ import { defineTool, withFileMutationQueue } from "@earendil-works/pi-coding-age
4
4
  import { Type } from "typebox";
5
5
  import { post, usageFrom, type MorphUsage } from "./api.ts";
6
6
 
7
- /** Fast Apply merges an edit snippet into an existing file. `auto` lets Morph pick the model. */
8
- export const APPLY_MODELS = ["auto", "morph-v3-fast", "morph-v3-large"] as const;
7
+ /**
8
+ * Fast Apply merges an edit snippet into an existing file. These are the models the `/code/apply`
9
+ * endpoint serves; it rejects anything else (including the documented `auto` and `morph-v3-large`)
10
+ * with `invalid_request_error`.
11
+ */
12
+ export const APPLY_MODELS = ["morph-v3-fast", "morph-v2", "morph-v0"] as const;
9
13
 
10
14
  export const ApplyParams = Type.Object({
11
15
  path: Type.String({ description: "File to edit, relative to the working directory" }),
@@ -18,7 +22,7 @@ export const ApplyParams = Type.Object({
18
22
  model: Type.Optional(
19
23
  Type.Union(
20
24
  APPLY_MODELS.map((model) => Type.Literal(model)),
21
- { description: "Fast Apply model. Default: auto." },
25
+ { description: "Fast Apply model. Default: morph-v3-fast." },
22
26
  ),
23
27
  ),
24
28
  });
@@ -39,7 +43,7 @@ export const applyTool = defineTool({
39
43
  annotations: { destructiveHint: true, openWorldHint: true },
40
44
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
41
45
  const file = path.resolve(ctx.cwd, params.path);
42
- const model = params.model ?? "auto";
46
+ const model = params.model ?? "morph-v3-fast";
43
47
  return withFileMutationQueue(file, async () => {
44
48
  let original: string;
45
49
  try {
package/tools/search.ts CHANGED
@@ -378,6 +378,32 @@ export async function spanText(root: string, spans: Span[]): Promise<string> {
378
378
  return parts.join("\n\n");
379
379
  }
380
380
 
381
+ /* ------------------------------------------------------------ text tool calls */
382
+
383
+ // The API no longer parses WarpGrep's output into structured `tool_calls`: the model emits
384
+ // Hermes-style text — `<tool_call>\n<function=read>\n<parameter=path>\n…\n</parameter>\n</function>\n</tool_call>`
385
+ // — in `content`. Parse those blocks into the structured shape the loop below works with.
386
+ // Arguments become a JSON string so `parseArgs` treats every call the same way.
387
+
388
+ const TOOL_CALL_RE = /<tool_call>\s*<function=([\w-]+)>([\s\S]*?)<\/function>\s*<\/tool_call>/g;
389
+ const PARAM_RE = /<parameter=([\w-]+)>([\s\S]*?)<\/parameter>/g;
390
+
391
+ export function parseTextToolCalls(content: string, idPrefix = "textcall"): ChatToolCall[] {
392
+ const calls: ChatToolCall[] = [];
393
+ for (const block of content.matchAll(TOOL_CALL_RE)) {
394
+ const args: Record<string, string> = {};
395
+ for (const param of (block[2] as string).matchAll(PARAM_RE)) {
396
+ args[param[1] as string] = (param[2] as string).trim();
397
+ }
398
+ calls.push({
399
+ id: `${idPrefix}-${calls.length}`,
400
+ type: "function",
401
+ function: { name: block[1], arguments: JSON.stringify(args) },
402
+ });
403
+ }
404
+ return calls;
405
+ }
406
+
381
407
  /* ----------------------------------------------------------------------- loop */
382
408
 
383
409
  export interface SearchResult {
@@ -418,7 +444,10 @@ export async function warpgrep(root: string, query: string, options: SearchOptio
418
444
  total.total_tokens = (total.total_tokens ?? 0) + (res.usage?.total_tokens ?? 0);
419
445
 
420
446
  const message = res.choices?.[0]?.message;
421
- const calls = message?.tool_calls ?? [];
447
+ let calls = message?.tool_calls ?? [];
448
+ if (!calls.length && typeof message?.content === "string" && message.content.includes("<tool_call>")) {
449
+ calls = parseTextToolCalls(message.content, `textcall-${turn}`);
450
+ }
422
451
  options.onTurn?.(turn, calls.map((call) => call.function?.name ?? "?"));
423
452
 
424
453
  const finish = calls.find((call) => (call.function?.name ?? "").toLowerCase() === "finish");