@computesdk/pi-morph 1.0.2 → 1.0.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/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  pi install npm:@computesdk/pi-morph # or: pi -e /path/to/pi-extensions/packages/pi-morph
7
7
  ```
8
8
 
9
- Set `MORPH_API_KEY` in your environment; one key covers both halves.
9
+ Run `/login morph` and paste your key, or set `MORPH_API_KEY` in your environment; one credential covers both halves (the models and the tools), and the environment wins when both are set.
10
10
 
11
11
  ## Models
12
12
 
package/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // Morph — open-weight coding models on an OpenAI-compatible API, plus specialized apply/grep/compact models.
2
- // Base URL: https://api.morphllm.com/v1 · Auth: Authorization: Bearer <MORPH_API_KEY>.
2
+ // Base URL: https://api.morphllm.com/v1 · Auth: `/login morph` (stored in auth.json) or MORPH_API_KEY in the environment.
3
3
  // Docs: https://docs.morphllm.com · Models: https://docs.morphllm.com/sdk/components/fast-models
4
4
  //
5
5
  // This is a Pi provider extension: `pi.registerProvider` adds the endpoint and its model catalog, and Pi's
@@ -14,11 +14,15 @@
14
14
  // Prices are USD per 1M tokens from https://www.morphllm.com/pricing (live at /api/models/json). Update them
15
15
  // there when they change.
16
16
 
17
- import type { ExtensionAPI, ProviderModelConfig } from "@earendil-works/pi-coding-agent";
17
+ 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";
18
20
 
19
- /** Pi's chat-model shape: the chat arm of the provider model union. */
20
- type ChatModel = Extract<ProviderModelConfig, { type?: "chat" }>;
21
- type ThinkingLevelMap = NonNullable<ChatModel["thinkingLevelMap"]>;
21
+ const ID = "morph";
22
+ const BASE_URL = "https://api.morphllm.com/v1";
23
+
24
+ /** Pi's chat-model shape for the one API Morph speaks. */
25
+ type ChatModel = Model<"openai-completions">;
22
26
  // Pi thinking levels (off/minimal/low/medium/high/xhigh/max) → the provider's `reasoning_effort` value.
23
27
  // Morph documents the ladder for DeepSeek: none | low | high | xhigh | max (`medium`→high, `minimal`→low).
24
28
  // For the other models `off` is not documented, so it is marked unsupported (`null`) rather than sent.
@@ -35,6 +39,9 @@ const model = (id: string, name: string, { input = ["text"], cost, effort = EFFO
35
39
  type: "chat",
36
40
  id,
37
41
  name,
42
+ provider: ID,
43
+ api: "openai-completions",
44
+ baseUrl: BASE_URL,
38
45
  reasoning: true,
39
46
  thinkingLevelMap: effort,
40
47
  input,
@@ -54,11 +61,17 @@ export const MODELS: ChatModel[] = [
54
61
  ];
55
62
 
56
63
  export default function morphProvider(pi: ExtensionAPI) {
57
- pi.registerProvider("morph", {
58
- name: "Morph",
59
- baseUrl: "https://api.morphllm.com/v1",
60
- apiKey: "$MORPH_API_KEY",
61
- api: "openai-completions",
62
- models: MODELS,
63
- });
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
+ );
64
77
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@computesdk/pi-morph",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
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",
@@ -47,10 +47,12 @@
47
47
  "test": "tsc --noEmit && node test.ts"
48
48
  },
49
49
  "peerDependencies": {
50
+ "@earendil-works/pi-ai": "*",
50
51
  "@earendil-works/pi-coding-agent": "*",
51
52
  "typebox": "*"
52
53
  },
53
54
  "devDependencies": {
55
+ "@earendil-works/pi-ai": "catalog:",
54
56
  "@earendil-works/pi-coding-agent": "catalog:",
55
57
  "@types/node": "catalog:",
56
58
  "typebox": "catalog:",
package/tools/api.ts CHANGED
@@ -5,6 +5,10 @@
5
5
  * Fast Apply, Compact, and WarpGrep have bespoke request shapes, so the tools call them directly.
6
6
  */
7
7
 
8
+ import fs from "node:fs";
9
+ import os from "node:os";
10
+ import path from "node:path";
11
+
8
12
  export const BASE_URL = "https://api.morphllm.com/v1";
9
13
 
10
14
  /** Morph reports OpenAI-style token counts. */
@@ -41,13 +45,28 @@ export function usageFrom(usage: MorphUsage | undefined): Usage {
41
45
  };
42
46
  }
43
47
 
44
- /** The provider reads the same variable, so one `MORPH_API_KEY` covers models and tools. */
48
+ /**
49
+ * One credential covers models and tools: `MORPH_API_KEY` in the environment, or the key
50
+ * `/login morph` stored in the agent directory's auth.json (the provider resolves the same two,
51
+ * stored first; here the env var wins so the shell can override a stored key).
52
+ */
45
53
  export function apiKey(): string {
46
- const key = process.env.MORPH_API_KEY?.trim();
47
- if (!key) throw new Error("MORPH_API_KEY is not set; Morph's tools need it");
54
+ const key = process.env.MORPH_API_KEY?.trim() || storedKey();
55
+ if (!key) throw new Error("MORPH_API_KEY is not set and no Morph credential is stored; set it or run /login morph");
48
56
  return key;
49
57
  }
50
58
 
59
+ /** Read the key `/login morph` wrote. Only literal keys: a "!command" credential is Pi-side resolution. */
60
+ function storedKey(): string | undefined {
61
+ try {
62
+ const dir = process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".pi", "agent");
63
+ const key = JSON.parse(fs.readFileSync(path.join(dir, "auth.json"), "utf8"))?.morph?.key;
64
+ return typeof key === "string" && !key.startsWith("!") ? key : undefined;
65
+ } catch {
66
+ return undefined;
67
+ }
68
+ }
69
+
51
70
  /** POST JSON to Morph. A non-2xx response becomes a tool error. */
52
71
  export async function post<T>(path: string, body: unknown, signal?: AbortSignal): Promise<T> {
53
72
  const res = await fetch(`${BASE_URL}${path}`, {