opencode-morphllm 0.0.6 → 0.0.7

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/.gitleaks.toml ADDED
@@ -0,0 +1,6 @@
1
+ title = "Gitleaks Config"
2
+
3
+ [[rules]]
4
+ id = "morph-key"
5
+ description = "Morph API Key"
6
+ regex = "sk-[A-Za-z0-9_-]{48}"
package/.husky/pre-commit CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env sh
2
2
  . "$(dirname -- "$0")/_/husky.sh"
3
3
 
4
- bunx lint-staged
4
+ bunx lint-staged
5
+
6
+ gitleaks protect --staged --config .gitleaks.toml --verbose || true
@@ -0,0 +1 @@
1
+ dist/
package/.prettierrc CHANGED
@@ -5,4 +5,4 @@
5
5
  "printWidth": 80,
6
6
  "tabWidth": 2,
7
7
  "useTabs": false
8
- }
8
+ }
package/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # MorphLLM OpenCode Plugin
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/opencode-morphllm?style=flat-square)](https://www.npmjs.com/package/opencode-morphllm)
4
+ [![downloads](https://img.shields.io/npm/dt/opencode-morphllm?style=flat-square)](https://www.npmjs.com/package/opencode-morphllm)
5
+
3
6
  This is an OpenCode Plugin for [MorphLLM](https://morphllm.com/). This plugin just adds in `edit_file` and `warpgrep_codebase_search` from MorphLLM to your agent configs as well as the intelligent model router for choosing different models based on the difficulty of the prompt.
4
7
 
5
8
  Github: https://github.com/VitoLin/opencode-morphllm
@@ -27,7 +30,8 @@ Example configs:
27
30
  "MORPH_MODEL_EASY": "github-copilot/gpt-5-mini",
28
31
  "MORPH_MODEL_MEDIUM": "opencode/minimax-m2.1-free",
29
32
  "MORPH_MODEL_HARD": "github-copilot/gemini-2.5-pro",
30
- "MORPH_ROUTER_ENABLED": true
33
+ "MORPH_ROUTER_ENABLED": true,
34
+ "MORPH_ROUTER_PROMPT_CACHING_AWARE": true
31
35
  }
32
36
  }
33
37
  ```
@@ -40,7 +44,8 @@ Legacy format (still supported):
40
44
  "MORPH_MODEL_EASY": "github-copilot/gpt-5-mini",
41
45
  "MORPH_MODEL_MEDIUM": "opencode/minimax-m2.1-free",
42
46
  "MORPH_MODEL_HARD": "github-copilot/gemini-2.5-pro",
43
- "MORPH_ROUTER_ENABLED": true
47
+ "MORPH_ROUTER_ENABLED": true,
48
+ "MORPH_ROUTER_PROMPT_CACHING_AWARE": true
44
49
  }
45
50
  ```
46
51
 
package/bun.lock CHANGED
@@ -10,6 +10,7 @@
10
10
  },
11
11
  "devDependencies": {
12
12
  "bun-types": "^1.3.6",
13
+ "coverage-badges-cli": "^2.2.0",
13
14
  "husky": "^8.0.3",
14
15
  "lint-staged": "^15.2.0",
15
16
  "prettier": "^3.2.5",
@@ -34,6 +35,12 @@
34
35
 
35
36
  "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
36
37
 
38
+ "@types/fs-extra": ["@types/fs-extra@11.0.4", "", { "dependencies": { "@types/jsonfile": "*", "@types/node": "*" } }, "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ=="],
39
+
40
+ "@types/jsonfile": ["@types/jsonfile@6.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ=="],
41
+
42
+ "@types/minimist": ["@types/minimist@1.2.5", "", {}, "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag=="],
43
+
37
44
  "@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="],
38
45
 
39
46
  "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],
@@ -62,6 +69,8 @@
62
69
 
63
70
  "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
64
71
 
72
+ "badgen": ["badgen@3.2.3", "", {}, "sha512-svDuwkc63E/z0ky3drpUppB83s/nlgDciH9m+STwwQoWyq7yCgew1qEfJ+9axkKdNq7MskByptWUN9j1PGMwFA=="],
73
+
65
74
  "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
66
75
 
67
76
  "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
@@ -92,6 +101,8 @@
92
101
 
93
102
  "commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
94
103
 
104
+ "coverage-badges-cli": ["coverage-badges-cli@2.2.0", "", { "dependencies": { "@types/fs-extra": "~11.0.0", "@types/minimist": "~1.2.2", "badgen": "~3.2.3", "fs-extra": "~11.2.0", "lodash.get": "^4.4.2", "mini-svg-data-uri": "^1.4.4", "minimist": "~1.2.5" }, "bin": { "coverage-badges": "bin/cli", "coverage-badges-cli": "bin/cli" } }, "sha512-7E7TtxiybFkK5/FRkzt4WTNZkopEv+hra7I7k52fnOD0YBweCnmV4b0FCCEvnO8567e+xCEZ3AdbIfeVlkh+hg=="],
105
+
95
106
  "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="],
96
107
 
97
108
  "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
@@ -144,6 +155,8 @@
144
155
 
145
156
  "formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="],
146
157
 
158
+ "fs-extra": ["fs-extra@11.2.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw=="],
159
+
147
160
  "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
148
161
 
149
162
  "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
@@ -156,6 +169,8 @@
156
169
 
157
170
  "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
158
171
 
172
+ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
173
+
159
174
  "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
160
175
 
161
176
  "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
@@ -196,12 +211,16 @@
196
211
 
197
212
  "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
198
213
 
214
+ "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
215
+
199
216
  "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
200
217
 
201
218
  "lint-staged": ["lint-staged@15.5.2", "", { "dependencies": { "chalk": "^5.4.1", "commander": "^13.1.0", "debug": "^4.4.0", "execa": "^8.0.1", "lilconfig": "^3.1.3", "listr2": "^8.2.5", "micromatch": "^4.0.8", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.7.0" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w=="],
202
219
 
203
220
  "listr2": ["listr2@8.3.3", "", { "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ=="],
204
221
 
222
+ "lodash.get": ["lodash.get@4.4.2", "", {}, "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="],
223
+
205
224
  "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="],
206
225
 
207
226
  "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
@@ -220,6 +239,8 @@
220
239
 
221
240
  "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
222
241
 
242
+ "mini-svg-data-uri": ["mini-svg-data-uri@1.4.4", "", { "bin": { "mini-svg-data-uri": "cli.js" } }, "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg=="],
243
+
223
244
  "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
224
245
 
225
246
  "minimisted": ["minimisted@2.0.1", "", { "dependencies": { "minimist": "^1.2.5" } }, "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA=="],
@@ -304,6 +325,8 @@
304
325
 
305
326
  "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
306
327
 
328
+ "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
329
+
307
330
  "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="],
308
331
 
309
332
  "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
package/bunfig.toml ADDED
@@ -0,0 +1,4 @@
1
+ [test]
2
+ coverage = true
3
+ coverageThreshold = 0.7
4
+ coverageReporter = ["lcov"]
package/dist/index.js CHANGED
@@ -1,17 +1,16 @@
1
1
  import { createBuiltinMcps } from './morph/mcps';
2
2
  import { createModelRouterHook } from './morph/router';
3
- import { MORPH_ROUTER_ENABLED } from './shared/config';
4
3
  const MorphOpenCodePlugin = async () => {
5
- const builtinMcps = createBuiltinMcps();
6
- const routerHook = MORPH_ROUTER_ENABLED ? createModelRouterHook() : {};
7
- return {
8
- config: async (currentConfig) => {
9
- currentConfig.mcp = {
10
- ...currentConfig.mcp,
11
- ...builtinMcps,
12
- };
13
- },
14
- ...routerHook,
15
- };
4
+ const builtinMcps = createBuiltinMcps();
5
+ const routerHook = createModelRouterHook();
6
+ return {
7
+ config: async (currentConfig) => {
8
+ currentConfig.mcp = {
9
+ ...currentConfig.mcp,
10
+ ...builtinMcps,
11
+ };
12
+ },
13
+ ...routerHook,
14
+ };
16
15
  };
17
16
  export default MorphOpenCodePlugin;
@@ -1,15 +1,15 @@
1
1
  import { API_KEY } from '../shared/config';
2
2
  export function createBuiltinMcps() {
3
- return {
4
- morph_mcp: {
5
- type: 'local',
6
- command: ['npx', '-y', '@morphllm/morphmcp'],
7
- environment: {
8
- MORPH_API_KEY: API_KEY,
9
- ENABLED_TOOLS: 'edit_file,warpgrep_codebase_search',
10
- },
11
- enabled: true,
12
- },
13
- };
3
+ return {
4
+ morph_mcp: {
5
+ type: 'local',
6
+ command: ['npx', '-y', '@morphllm/morphmcp'],
7
+ environment: {
8
+ MORPH_API_KEY: API_KEY,
9
+ ENABLED_TOOLS: 'edit_file,warpgrep_codebase_search',
10
+ },
11
+ enabled: true,
12
+ },
13
+ };
14
14
  }
15
15
  export default createBuiltinMcps;
@@ -1,39 +1,39 @@
1
1
  import { describe, it, expect, vi } from 'bun:test';
2
2
  vi.mock('../shared/config', () => ({
3
- API_KEY: 'test-api-key-123',
3
+ API_KEY: 'test-api-key-123',
4
4
  }));
5
5
  import { createBuiltinMcps } from './mcps';
6
6
  describe('mcps.ts', () => {
7
- describe('createBuiltinMcps', () => {
8
- it('should create morph_mcp configuration', () => {
9
- const mcps = createBuiltinMcps();
10
- expect(mcps).toHaveProperty('morph_mcp');
11
- expect(mcps.morph_mcp.type).toBe('local');
12
- expect(mcps.morph_mcp.enabled).toBe(true);
7
+ describe('createBuiltinMcps', () => {
8
+ it('should create morph_mcp configuration', () => {
9
+ const mcps = createBuiltinMcps();
10
+ expect(mcps).toHaveProperty('morph_mcp');
11
+ expect(mcps.morph_mcp.type).toBe('local');
12
+ expect(mcps.morph_mcp.enabled).toBe(true);
13
+ });
14
+ it('should set correct command for morph_mcp', () => {
15
+ const mcps = createBuiltinMcps();
16
+ expect(mcps.morph_mcp.command).toEqual([
17
+ 'npx',
18
+ '-y',
19
+ '@morphllm/morphmcp',
20
+ ]);
21
+ });
22
+ it('should set MORPH_API_KEY in environment', () => {
23
+ const mcps = createBuiltinMcps();
24
+ const env = mcps.morph_mcp.environment;
25
+ expect(env).toBeDefined();
26
+ expect(env?.MORPH_API_KEY).toBe('test-api-key-123');
27
+ });
28
+ it('should set ENABLED_TOOLS environment variable', () => {
29
+ const mcps = createBuiltinMcps();
30
+ const env = mcps.morph_mcp.environment;
31
+ expect(env).toBeDefined();
32
+ expect(env?.ENABLED_TOOLS).toBe('edit_file,warpgrep_codebase_search');
33
+ });
34
+ it('should only create morph_mcp key', () => {
35
+ const mcps = createBuiltinMcps();
36
+ expect(Object.keys(mcps)).toEqual(['morph_mcp']);
37
+ });
13
38
  });
14
- it('should set correct command for morph_mcp', () => {
15
- const mcps = createBuiltinMcps();
16
- expect(mcps.morph_mcp.command).toEqual([
17
- 'npx',
18
- '-y',
19
- '@morphllm/morphmcp',
20
- ]);
21
- });
22
- it('should set MORPH_API_KEY in environment', () => {
23
- const mcps = createBuiltinMcps();
24
- const env = mcps.morph_mcp.environment;
25
- expect(env).toBeDefined();
26
- expect(env?.MORPH_API_KEY).toBe('test-api-key-123');
27
- });
28
- it('should set ENABLED_TOOLS environment variable', () => {
29
- const mcps = createBuiltinMcps();
30
- const env = mcps.morph_mcp.environment;
31
- expect(env).toBeDefined();
32
- expect(env?.ENABLED_TOOLS).toBe('edit_file,warpgrep_codebase_search');
33
- });
34
- it('should only create morph_mcp key', () => {
35
- const mcps = createBuiltinMcps();
36
- expect(Object.keys(mcps)).toEqual(['morph_mcp']);
37
- });
38
- });
39
39
  });
@@ -1,27 +1,22 @@
1
1
  import type { Part, UserMessage } from '@opencode-ai/sdk';
2
2
  import type { RouterInput, RawRouterResult } from '@morphllm/morphsdk';
3
3
  export declare function createModelRouterHook(): {
4
- 'chat.message': (
5
- input: {
6
- sessionID: string;
7
- agent?: string;
8
- model?: {
9
- providerID: string;
10
- modelID: string;
11
- };
12
- messageID?: string;
13
- variant?: string;
14
- classify?: (args: RouterInput) => Promise<RawRouterResult>;
15
- },
16
- output: {
17
- message: UserMessage;
18
- parts: Part[];
19
- }
20
- ) => Promise<void>;
4
+ 'chat.message': (input: {
5
+ sessionID: string;
6
+ agent?: string;
7
+ model?: {
8
+ providerID: string;
9
+ modelID: string;
10
+ };
11
+ messageID?: string;
12
+ variant?: string;
13
+ classify?: (args: RouterInput) => Promise<RawRouterResult>;
14
+ }, output: {
15
+ message: UserMessage;
16
+ parts: Part[];
17
+ }) => Promise<void>;
21
18
  };
22
- export declare function extractPromptText(
23
- parts: Array<{
19
+ export declare function extractPromptText(parts: Array<{
24
20
  type: string;
25
21
  text?: string;
26
- }>
27
- ): string;
22
+ }>): string;
@@ -1,69 +1,65 @@
1
1
  import { MorphClient } from '@morphllm/morphsdk';
2
- import {
3
- API_KEY,
4
- MORPH_MODEL_EASY,
5
- MORPH_MODEL_MEDIUM,
6
- MORPH_MODEL_HARD,
7
- MORPH_MODEL_DEFAULT,
8
- MORPH_ROUTER_ONLY_FIRST_MESSAGE,
9
- } from '../shared/config';
2
+ import { API_KEY, MORPH_MODEL_EASY, MORPH_MODEL_MEDIUM, MORPH_MODEL_HARD, MORPH_MODEL_DEFAULT, MORPH_ROUTER_PROMPT_CACHING_AWARE, MORPH_ROUTER_ENABLED, } from '../shared/config';
10
3
  // Lazy initialization to allow mocking in tests
11
4
  let morph = null;
12
5
  const sessionsWithModelSelected = new Set();
13
6
  function getMorphClient() {
14
- if (!morph) {
15
- morph = new MorphClient({ apiKey: API_KEY });
16
- }
17
- return morph;
7
+ if (!morph) {
8
+ morph = new MorphClient({ apiKey: API_KEY });
9
+ }
10
+ return morph;
18
11
  }
19
12
  function parseModel(s) {
20
- if (!s) return { providerID: '', modelID: '' };
21
- const [providerID = '', modelID = ''] = s.split('/');
22
- return { providerID, modelID };
13
+ if (!s)
14
+ return { providerID: '', modelID: '' };
15
+ const [providerID = '', modelID = ''] = s.split('/');
16
+ return { providerID, modelID };
23
17
  }
24
18
  function pickModelForDifficulty(difficulty) {
25
- const key = String(difficulty).toLowerCase();
26
- switch (key) {
27
- case 'easy':
28
- return parseModel(MORPH_MODEL_EASY);
29
- case 'medium':
30
- return parseModel(MORPH_MODEL_MEDIUM);
31
- case 'hard':
32
- return parseModel(MORPH_MODEL_HARD);
33
- default:
34
- return parseModel(MORPH_MODEL_DEFAULT);
35
- }
19
+ const key = String(difficulty).toLowerCase();
20
+ switch (key) {
21
+ case 'easy':
22
+ return parseModel(MORPH_MODEL_EASY);
23
+ case 'medium':
24
+ return parseModel(MORPH_MODEL_MEDIUM);
25
+ case 'hard':
26
+ return parseModel(MORPH_MODEL_HARD);
27
+ default:
28
+ return parseModel(MORPH_MODEL_DEFAULT);
29
+ }
36
30
  }
37
31
  export function createModelRouterHook() {
38
- return {
39
- 'chat.message': async (input, output) => {
40
- input.model = input.model ?? { providerID: '', modelID: '' };
41
- if (MORPH_ROUTER_ONLY_FIRST_MESSAGE) {
42
- if (sessionsWithModelSelected.has(input.sessionID)) {
43
- return;
44
- }
45
- }
46
- const promptText = extractPromptText(output.parts);
47
- const classifier =
48
- input.classify ??
49
- ((args) => getMorphClient().routers.raw.classify(args));
50
- const classification = await classifier({
51
- input: promptText,
52
- });
53
- const chosen = pickModelForDifficulty(classification?.difficulty);
54
- const finalProviderID = chosen.providerID || input.model.providerID;
55
- const finalModelID = chosen.modelID || input.model.modelID;
56
- input.model.providerID = finalProviderID;
57
- input.model.modelID = finalModelID;
58
- if (MORPH_ROUTER_ONLY_FIRST_MESSAGE) {
59
- sessionsWithModelSelected.add(input.sessionID);
60
- }
61
- },
62
- };
32
+ return {
33
+ 'chat.message': async (input, output) => {
34
+ input.model = input.model ?? { providerID: '', modelID: '' };
35
+ if (!MORPH_ROUTER_ENABLED) {
36
+ return;
37
+ }
38
+ if (MORPH_ROUTER_PROMPT_CACHING_AWARE) {
39
+ if (sessionsWithModelSelected.has(input.sessionID)) {
40
+ return;
41
+ }
42
+ }
43
+ const promptText = extractPromptText(output.parts);
44
+ const classifier = input.classify ??
45
+ ((args) => getMorphClient().routers.raw.classify(args));
46
+ const classification = await classifier({
47
+ input: promptText,
48
+ });
49
+ const chosen = pickModelForDifficulty(classification?.difficulty);
50
+ const finalProviderID = chosen.providerID || input.model.providerID;
51
+ const finalModelID = chosen.modelID || input.model.modelID;
52
+ input.model.providerID = finalProviderID;
53
+ input.model.modelID = finalModelID;
54
+ if (MORPH_ROUTER_ENABLED && MORPH_ROUTER_PROMPT_CACHING_AWARE) {
55
+ sessionsWithModelSelected.add(input.sessionID);
56
+ }
57
+ },
58
+ };
63
59
  }
64
60
  export function extractPromptText(parts) {
65
- return parts
66
- .filter((p) => p.type === 'text')
67
- .map((p) => p.text || '')
68
- .join(' ');
61
+ return parts
62
+ .filter((p) => p.type === 'text')
63
+ .map((p) => p.text || '')
64
+ .join(' ');
69
65
  }