@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 +19 -16
- package/package.json +1 -1
- package/tools/apply.ts +8 -4
- package/tools/search.ts +30 -1
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-
|
|
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 {
|
|
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
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
+
"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
|
-
/**
|
|
8
|
-
|
|
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:
|
|
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 ?? "
|
|
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
|
-
|
|
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");
|