@nanonets/graft 0.4.5 → 0.5.1

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.
Files changed (116) hide show
  1. package/README.md +60 -14
  2. package/dist/ai/crux.d.ts +20 -4
  3. package/dist/ai/crux.d.ts.map +1 -1
  4. package/dist/ai/crux.js +39 -41
  5. package/dist/ai/crux.js.map +1 -1
  6. package/dist/ai/llm/anthropic.d.ts +37 -0
  7. package/dist/ai/llm/anthropic.d.ts.map +1 -0
  8. package/dist/ai/llm/anthropic.js +141 -0
  9. package/dist/ai/llm/anthropic.js.map +1 -0
  10. package/dist/ai/llm/factory.d.ts +19 -0
  11. package/dist/ai/llm/factory.d.ts.map +1 -0
  12. package/dist/ai/llm/factory.js +20 -0
  13. package/dist/ai/llm/factory.js.map +1 -0
  14. package/dist/ai/llm/openai.d.ts +33 -0
  15. package/dist/ai/llm/openai.d.ts.map +1 -0
  16. package/dist/ai/llm/openai.js +146 -0
  17. package/dist/ai/llm/openai.js.map +1 -0
  18. package/dist/ai/llm/types.d.ts +113 -0
  19. package/dist/ai/llm/types.d.ts.map +1 -0
  20. package/dist/ai/llm/types.js +22 -0
  21. package/dist/ai/llm/types.js.map +1 -0
  22. package/dist/ai/providers.d.ts +37 -15
  23. package/dist/ai/providers.d.ts.map +1 -1
  24. package/dist/ai/providers.js +30 -5
  25. package/dist/ai/providers.js.map +1 -1
  26. package/dist/ai/summarize.d.ts +4 -4
  27. package/dist/ai/summarize.d.ts.map +1 -1
  28. package/dist/ai/summarize.js +6 -13
  29. package/dist/ai/summarize.js.map +1 -1
  30. package/dist/ai/synthesize.d.ts +12 -4
  31. package/dist/ai/synthesize.d.ts.map +1 -1
  32. package/dist/ai/synthesize.js +19 -34
  33. package/dist/ai/synthesize.js.map +1 -1
  34. package/dist/ask/ask.d.ts +33 -0
  35. package/dist/ask/ask.d.ts.map +1 -1
  36. package/dist/ask/ask.js +119 -9
  37. package/dist/ask/ask.js.map +1 -1
  38. package/dist/ask/fuse.d.ts +96 -0
  39. package/dist/ask/fuse.d.ts.map +1 -0
  40. package/dist/ask/fuse.js +184 -0
  41. package/dist/ask/fuse.js.map +1 -0
  42. package/dist/ask/graphrank.d.ts.map +1 -1
  43. package/dist/ask/graphrank.js +1 -13
  44. package/dist/ask/graphrank.js.map +1 -1
  45. package/dist/claude/format.d.ts +17 -0
  46. package/dist/claude/format.d.ts.map +1 -1
  47. package/dist/claude/format.js +49 -2
  48. package/dist/claude/format.js.map +1 -1
  49. package/dist/claude/hooks.d.ts.map +1 -1
  50. package/dist/claude/hooks.js +16 -15
  51. package/dist/claude/hooks.js.map +1 -1
  52. package/dist/claude/skill-template.d.ts.map +1 -1
  53. package/dist/claude/skill-template.js +28 -13
  54. package/dist/claude/skill-template.js.map +1 -1
  55. package/dist/claude/state.d.ts +4 -0
  56. package/dist/claude/state.d.ts.map +1 -1
  57. package/dist/claude/state.js +1 -1
  58. package/dist/claude/state.js.map +1 -1
  59. package/dist/cli.d.ts +2 -0
  60. package/dist/cli.d.ts.map +1 -1
  61. package/dist/cli.js +100 -9
  62. package/dist/cli.js.map +1 -1
  63. package/dist/engine.d.ts +5 -3
  64. package/dist/engine.d.ts.map +1 -1
  65. package/dist/engine.js +31 -20
  66. package/dist/engine.js.map +1 -1
  67. package/dist/graph/bindings.d.ts +42 -0
  68. package/dist/graph/bindings.d.ts.map +1 -0
  69. package/dist/graph/bindings.js +304 -0
  70. package/dist/graph/bindings.js.map +1 -0
  71. package/dist/graph/extract.d.ts +17 -1
  72. package/dist/graph/extract.d.ts.map +1 -1
  73. package/dist/graph/extract.js +47 -5
  74. package/dist/graph/extract.js.map +1 -1
  75. package/dist/graph/map.d.ts +71 -0
  76. package/dist/graph/map.d.ts.map +1 -0
  77. package/dist/graph/map.js +150 -0
  78. package/dist/graph/map.js.map +1 -0
  79. package/dist/graph/relations.d.ts +14 -0
  80. package/dist/graph/relations.d.ts.map +1 -0
  81. package/dist/graph/relations.js +8 -0
  82. package/dist/graph/relations.js.map +1 -0
  83. package/dist/graph/resolve.d.ts.map +1 -1
  84. package/dist/graph/resolve.js +0 -0
  85. package/dist/graph/resolve.js.map +1 -1
  86. package/dist/graph/scopes.d.ts +15 -0
  87. package/dist/graph/scopes.d.ts.map +1 -0
  88. package/dist/graph/scopes.js +346 -0
  89. package/dist/graph/scopes.js.map +1 -0
  90. package/dist/graph/traverse.d.ts.map +1 -1
  91. package/dist/graph/traverse.js +1 -12
  92. package/dist/graph/traverse.js.map +1 -1
  93. package/dist/hosts/instructions.d.ts.map +1 -1
  94. package/dist/hosts/instructions.js +13 -6
  95. package/dist/hosts/instructions.js.map +1 -1
  96. package/dist/index.d.ts +9 -3
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +8 -4
  99. package/dist/index.js.map +1 -1
  100. package/dist/mcp/server.d.ts +1 -1
  101. package/dist/mcp/server.d.ts.map +1 -1
  102. package/dist/mcp/server.js +2 -2
  103. package/dist/mcp/server.js.map +1 -1
  104. package/dist/mcp/tools.d.ts +1 -1
  105. package/dist/mcp/tools.d.ts.map +1 -1
  106. package/dist/mcp/tools.js +80 -7
  107. package/dist/mcp/tools.js.map +1 -1
  108. package/dist/search/grep-cli.d.ts +32 -0
  109. package/dist/search/grep-cli.d.ts.map +1 -0
  110. package/dist/search/grep-cli.js +101 -0
  111. package/dist/search/grep-cli.js.map +1 -0
  112. package/dist/search/grep.d.ts +55 -0
  113. package/dist/search/grep.d.ts.map +1 -0
  114. package/dist/search/grep.js +126 -0
  115. package/dist/search/grep.js.map +1 -0
  116. package/package.json +2 -1
package/README.md CHANGED
@@ -17,8 +17,8 @@
17
17
  <img src="https://img.shields.io/badge/telemetry-none-546FFF?style=for-the-badge" />
18
18
  </p>
19
19
 
20
- <!-- placeholder: numbers pending a committed benchmark run, see Benchmark below -->
21
- **31% fewer tool calls · 23% less cost · 17% lower latency.** That is what an agent gets from reading the graph first instead of going in cold.
20
+ <!-- numbers from the committed run in bench/results/ (2026-07-22, 162 agent runs, 2 repos) — see Benchmark below -->
21
+ **32% less cost · 46% fewer tool calls · 60% lower latency** with the graph bundle pushed up front, at equal correctness — or **16% less cost with correctness up 5 points** when the agent pulls from the graph on demand. Measured against the same agent going in cold.
22
22
 
23
23
  </div>
24
24
 
@@ -113,7 +113,7 @@ Graft builds that understanding **once** and writes it into your repo as a folde
113
113
  - **A real graph you can read.** No embeddings, no similarity search, no index to keep warm. The graph is a set of linked files your agent opens, greps, and follows, exactly the way it reads any other file in the repo.
114
114
  - **Grafted into git.** The graph is just files in `graft/`. Commit it, and anyone who clones the repo has it. No database, no server, no setup. Git does the syncing, and a stale graph shows up as a diff in review instead of rotting in some external store.
115
115
  - **The diff lives with the code.** When a change moves things around, you see it in the graph diff in the same pull request, right next to the code that caused it.
116
- - **Your key, your model.** Summaries are written by a model you pick on [OpenRouter](https://openrouter.ai), under your own key. The structural code graph (`graft build`, `graft check`) is deterministic tree-sitter and never calls a model at all.
116
+ - **Your provider, your key, your model.** Summaries are written by any provider you choose — OpenAI, Anthropic (native), OpenRouter, Fireworks, Groq, a LiteLLM proxy, or a local model — under your own key. The structural code graph (`graft build`, `graft check`) is deterministic tree-sitter and never calls a model at all.
117
117
 
118
118
  ---
119
119
 
@@ -171,7 +171,7 @@ When the wiring graph exists, `graft check` validates it too — structural drif
171
171
  ## What runs where
172
172
 
173
173
  - **On your machine, no key, no network:** the structural code graph. `graft build` (wiring graph + per-file cards), `graft check`, and `graft ask` are deterministic tree-sitter — they never call a model.
174
- - **Through your OpenRouter key:** the LLM-written parts — `graft build --deep` adds the concept nodes (file summaries + node synthesis) and the per-symbol summaries and cruxes. Set `OPENROUTER_API_KEY` and optionally pick the model with `GRAFT_OPENROUTER_MODEL` (default: `openai/gpt-4o-mini`).
174
+ - **Through your provider key:** the LLM-written parts — `graft build --deep` adds the concept nodes (file summaries + node synthesis) and the per-symbol summaries and cruxes. graft is vendor-neutral: set `GRAFT_PROVIDER` (`openai` for any OpenAI-compatible endpoint, or `anthropic` for the native API), your `GRAFT_API_KEY`, `GRAFT_MODEL`, and — for the `openai` wire format — `GRAFT_BASE_URL` to point at OpenRouter, Fireworks, Groq, a LiteLLM proxy, a local server, or OpenAI itself. Or pass `--provider/--model/--api-key/--base-url` on the command line. (`OPENROUTER_API_KEY` still works as a deprecated fallback.)
175
175
  - **No telemetry** and no analytics — the only network calls are the LLM requests you configured.
176
176
 
177
177
  See [`.env.example`](.env.example) for the full list of settings (model, base URL, graph directory).
@@ -192,6 +192,13 @@ graft callers <symbol> [dir] # who calls/references/imports/implements/e
192
192
  graft callees <symbol> [dir] # what a symbol calls/references/imports/implements/extends (no LLM, no key)
193
193
  graft impact <symbol> [dir] -d N # BFS over incoming edges — who breaks if this symbol changes (no LLM, no key)
194
194
 
195
+ graft grep "<regex>" [dir] # exhaustive regex search over indexed files, grouped by enclosing symbol (no LLM, no key)
196
+ graft grep "<regex>" --in <path> # narrow to files whose path contains this substring
197
+ graft grep "<regex>" -i --fixed # case-insensitive; treat the pattern as a literal string, not a regex
198
+
199
+ graft map [dir] # token-budgeted repo orientation — dir clusters, hubs, hotspots (no LLM, no key)
200
+ graft map --max-dirs N # raise/lower the number of directories shown
201
+
195
202
  graft check [dir] # fail (exit 1) if graft/ has drifted from the code
196
203
  graft check --json # print the drift report as JSON
197
204
 
@@ -212,6 +219,44 @@ graft --dir <path> # use a context dir other than <repo>/graft
212
219
  graft --version, -v # print the installed version and exit
213
220
  ```
214
221
 
222
+ Method calls resolve through the receiver's type — constructor assignments
223
+ (`self.router = APIRouter()`) and type annotations, not just the call-site
224
+ name — so `callers`/`impact`/`grep --in` return calls bound to the right
225
+ type on method-heavy code, not every method anywhere with that name.
226
+
227
+ ## Search & orient (`graft grep` / `graft map`)
228
+
229
+ `graft grep "<regex>"` is exhaustive over every indexed file and groups hits
230
+ by enclosing symbol, ranked by the same in-edge coupling `graft map` uses —
231
+ built for "every occurrence of this pattern" tasks where `graft ask`'s
232
+ ranked top-N isn't enough:
233
+
234
+ ```
235
+ "NEEDLE" — 2 hits in 2 symbols across 1 files (searched 1 indexed files)
236
+
237
+ heavilyCalled · function · src/a.ts:L1-L3 · 3 in-edges
238
+ L2: console.log("NEEDLE hit in heavilyCalled");
239
+
240
+ rarelyCalled · function · src/a.ts:L4-L6 · 0 in-edges
241
+ L5: console.log("NEEDLE hit in rarelyCalled");
242
+ ```
243
+
244
+ `graft map` is a token-budgeted first look at a repo — directory clusters
245
+ with file/symbol counts, each dir's local hubs, and the global hotspots —
246
+ all ranked by in-degree, no LLM, no key:
247
+
248
+ ```
249
+ repo map — 105 files · 582 symbols · 1815 edges · typescript
250
+
251
+ src/ 54 files · 414 symbols hubs: set (bindings.ts, 38←), contextDirFor (node-file.ts, 11←), wiringPath (write.ts, 10←)
252
+ test/ 36 files · 68 symbols hubs: edge (graph-traverse.test.ts, 4←), graphOf (graph-traverse.test.ts, 4←), nodeStub (graph-traverse.test.ts, 3←)
253
+ viewer/ 5 files · 58 symbols hubs: $ (main.ts, 9←), activeGraph (main.ts, 5←), cvar (data.ts, 5←)
254
+ bench/ 8 files · 42 symbols hubs: makeClient (llm.ts, 3←), buildMarkdown (report.ts, 2←), judge (judge.ts, 2←)
255
+ scripts/ 2 files · 0 symbols
256
+
257
+ hotspots: set · method · src/graph/bindings.ts:L21-L23 · 38← contextDirFor · function · src/context/node-file.ts:L100-L103 · 11← wiringPath · function · src/graph/write.ts:L20-L22 · 10← ...
258
+ ```
259
+
215
260
  ## Visualize it (`graft viz`)
216
261
 
217
262
  `graft viz` opens a local, interactive view of both graphs — no install, no dev
@@ -245,22 +290,23 @@ normalized on load — no regeneration needed.
245
290
 
246
291
  ## Benchmark
247
292
 
248
- <!-- placeholder: fill from a committed bench/results/ run before launch -->
293
+ The claim Graft has to earn is simple: an agent that gets its context from the graph is cheaper and faster without getting more answers wrong. The harness runs every task through three arms of the same Claude Sonnet 5 agent with the same file tools: **cold** (explores from zero), **graph (push)** (a `graft ask --source` bundle injected up front), and **graft (pull)** (graft_ask/graft_skeleton tools, nothing injected — context paid for only when asked). An Opus 4.8 judge scores correctness with a required-keyword floor so a fast-but-wrong answer cannot win. Cost is cache-aware (reads ≈0.1×, writes 1.25×) — the billing model agents actually run under.
249
294
 
250
- The claim Graft has to earn is simple: an agent that reads the graph first is cheaper and faster without getting more answers wrong. The harness runs every task twice through the same agent with the same file tools. One run is **cold** (it explores from zero) and one is **graph** (it gets the `graft/` bundle up front). A separate model judges correctness, with a required-keyword floor so a fast-but-wrong answer cannot win.
295
+ Committed run: `bench/results/2026-07-22T13-19-14-246Z` — 162 runs across two repos (graft itself and a real Node/Express auth service), 3 trials each, tasks split between localized (single-file) and multi-file questions.
251
296
 
252
- | Metric | Cold | Graph | Change |
297
+ | Metric (mean/task) | Cold | Graph (push) | Graft (pull) |
253
298
  |---|---|---|---|
254
- | Tool calls | _TBD_ | _TBD_ | **−31%** |
255
- | Cost | _TBD_ | _TBD_ | **−23%** |
256
- | Latency | _TBD_ | _TBD_ | **−17%** |
257
- | Correctness | _TBD_ | _TBD_ | _at least as good as cold_ |
299
+ | Cost ($) | 0.0429 | **0.0292 (−32%)** | 0.0362 (−16%) |
300
+ | Uncached input tokens | 8,070 | **4,650 (−42%)** | 5,965 (−26%) |
301
+ | Tool calls | 4.2 | **2.3 (−46%)** | 4.2 (−1%) |
302
+ | Latency (s) | 39.8 | **15.8 (−60%)** | 39.5 (−1%) |
303
+ | Correctness | 93% | 93% | **98% (+5 pts)** |
258
304
 
259
- _The numbers above are placeholders pending a committed run._ Reproduce it yourself:
305
+ Push is the cost/latency win; pull is the correctness win (and never lost to cold anywhere, including single-file tasks). On one corpus push traded 3 correctness points for its savings — the per-corpus and per-locality splits are in the committed results, not averaged away. Reproduce it yourself:
260
306
 
261
307
  ```bash
262
- npm run bench -- --smoke # 1 corpus, 1 task, a quick plumbing check
263
- npm run bench # full: all corpora, all tasks
308
+ npm run bench -- --smoke # 1 corpus, 1 task, all arms — a quick plumbing check
309
+ npm run bench # full: all corpora, all tasks, 3 arms × 3 trials
264
310
  ```
265
311
 
266
312
  See [`bench/README.md`](bench/README.md) for the method and how to add your own repo.
package/dist/ai/crux.d.ts CHANGED
@@ -1,3 +1,20 @@
1
+ /**
2
+ * Tier-2 "meaning" call for the code graph — batched one request per file.
3
+ *
4
+ * Given a source file (with 1-based line numbers) and the list of definitions in
5
+ * it, one call returns, for each definition:
6
+ * 1. `summary` — one plain-English sentence: what the symbol is *for*, at the
7
+ * business-logic level, not a restatement of its signature.
8
+ * 2. `crux_start`/`crux_end` — the smallest contiguous range of FILE line
9
+ * numbers (inside that symbol's own span) that a reviewer must read to see
10
+ * the decision or rule the code encodes. `0/0` means there is no single
11
+ * crux (a trivial getter, a plain data holder).
12
+ *
13
+ * Batching per file means N definitions cost one request, not N — and the model
14
+ * sees each symbol's neighbours, which sharpens the summaries. Line numbers are
15
+ * consumed once, at write time, to slice the crux text verbatim from source.
16
+ */
17
+ import type { ChatModel } from "./llm/types.js";
1
18
  import type { Kind } from "../graph/types.js";
2
19
  /** One definition we want described, located by its line span within the file. */
3
20
  export interface NodeRef {
@@ -21,11 +38,10 @@ export interface NodeCrux {
21
38
  export interface CruxSummarizer {
22
39
  describeFile(input: FileCruxInput): Promise<NodeCrux[]>;
23
40
  }
24
- /** Crux summarizer backed by OpenRouter's OpenAI-compatible chat API. */
25
- export declare class OpenRouterCruxSummarizer implements CruxSummarizer {
26
- private client;
41
+ /** Crux summarizer backed by any {@link ChatModel} via forced tool calling. */
42
+ export declare class ChatCruxSummarizer implements CruxSummarizer {
27
43
  private model;
28
- constructor(apiKey: string, model: string, baseUrl?: string);
44
+ constructor(model: ChatModel);
29
45
  describeFile(input: FileCruxInput): Promise<NodeCrux[]>;
30
46
  }
31
47
  //# sourceMappingURL=crux.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"crux.d.ts","sourceRoot":"","sources":["../../src/ai/crux.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAE9C,kFAAkF;AAClF,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,IAAI,CAAC;IACX,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,OAAO,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;CACzD;AAwDD,yEAAyE;AACzE,qBAAa,wBAAyB,YAAW,cAAc;IAC7D,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAAS;gBAEV,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,SAAiC;IAS7E,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;CAa9D"}
1
+ {"version":3,"file":"crux.d.ts","sourceRoot":"","sources":["../../src/ai/crux.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAE9C,kFAAkF;AAClF,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,IAAI,CAAC;IACX,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,OAAO,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;CACzD;AAwED,+EAA+E;AAC/E,qBAAa,kBAAmB,YAAW,cAAc;IAC3C,OAAO,CAAC,KAAK;gBAAL,KAAK,EAAE,SAAS;IAE9B,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;CAoB9D"}
package/dist/ai/crux.js CHANGED
@@ -1,33 +1,32 @@
1
- /**
2
- * Tier-2 "meaning" call for the code graph — batched one request per file.
3
- *
4
- * Given a source file (with 1-based line numbers) and the list of definitions in
5
- * it, one call returns, for each definition:
6
- * 1. `summary` — one plain-English sentence: what the symbol is *for*, at the
7
- * business-logic level, not a restatement of its signature.
8
- * 2. `crux_start`/`crux_end` — the smallest contiguous range of FILE line
9
- * numbers (inside that symbol's own span) that a reviewer must read to see
10
- * the decision or rule the code encodes. `0/0` means there is no single
11
- * crux (a trivial getter, a plain data holder).
12
- *
13
- * Batching per file means N definitions cost one request, not N — and the model
14
- * sees each symbol's neighbours, which sharpens the summaries. Line numbers are
15
- * consumed once, at write time, to slice the crux text verbatim from source.
16
- */
17
- import OpenAI from "openai";
18
1
  const SYSTEM_PROMPT = `You explain code definitions for a code graph that helps engineers navigate a codebase.
19
2
 
20
- You are given ONE source file with 1-based line numbers, and a list of TARGET definitions in it. For every target, return its purpose and the line range of its core logic.
21
-
22
- Return STRICT JSON, exactly:
23
- { "symbols": [ { "id": string, "summary": string, "crux_start": number, "crux_end": number }, ... ] }
3
+ You are given ONE source file with 1-based line numbers, and a list of TARGET definitions in it. For every target, record its purpose and the line range of its core logic via the record_symbols tool.
24
4
 
25
5
  Rules:
26
6
  - Emit exactly one entry per target id given, using that id verbatim.
27
7
  - summary: ONE sentence — what the symbol is FOR at the business-logic level. Say what problem it solves or rule it enforces, not what its signature already says.
28
8
  - crux_start / crux_end: FILE line numbers (as shown), inside that symbol's own line range. Pick the SINGLE most important contiguous span — the core branch, formula, guard, or state change. Keep it TIGHT: at most ~8 lines, and NEVER the whole function. If you can't narrow it below that, the symbol has no distinct crux — use 0/0.
29
- - Skip boilerplate, logging, and plumbing. If a symbol has no meaningful crux (trivial getter, data holder, one-line delegation, or logic spread evenly with no focal point), use "crux_start": 0 and "crux_end": 0.
30
- - Output ONLY the JSON object. No prose, no code fences.`;
9
+ - Skip boilerplate, logging, and plumbing. If a symbol has no meaningful crux (trivial getter, data holder, one-line delegation, or logic spread evenly with no focal point), use "crux_start": 0 and "crux_end": 0.`;
10
+ const RECORD_TOOL = "record_symbols";
11
+ const SYMBOLS_SCHEMA = {
12
+ type: "object",
13
+ properties: {
14
+ symbols: {
15
+ type: "array",
16
+ items: {
17
+ type: "object",
18
+ properties: {
19
+ id: { type: "string" },
20
+ summary: { type: "string" },
21
+ crux_start: { type: "number" },
22
+ crux_end: { type: "number" },
23
+ },
24
+ required: ["id", "summary", "crux_start", "crux_end"],
25
+ },
26
+ },
27
+ },
28
+ required: ["symbols"],
29
+ };
31
30
  /** Cap the file text sent per request so one huge file can't blow the context. */
32
31
  const MAX_CODE_CHARS = 18_000;
33
32
  function numberLines(source) {
@@ -44,11 +43,9 @@ function userContent(input) {
44
43
  .join("\n");
45
44
  return `FILE: ${input.path}\n\n${numberLines(input.source)}\n\nTARGETS:\n${targets}`;
46
45
  }
47
- /** Best-effort parse of a model's JSON reply into a {@link NodeCrux} list. */
48
- function parseResults(raw) {
49
- const text = raw.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
50
- const obj = JSON.parse(text);
51
- if (!Array.isArray(obj.symbols))
46
+ /** Normalize the tool's parsed argument object into a {@link NodeCrux} list. */
47
+ function parseResults(obj) {
48
+ if (!obj || !Array.isArray(obj.symbols))
52
49
  return [];
53
50
  const num = (v) => (typeof v === "number" && Number.isFinite(v) ? Math.trunc(v) : 0);
54
51
  return obj.symbols
@@ -61,31 +58,32 @@ function parseResults(raw) {
61
58
  crux_end: num(s.crux_end),
62
59
  }));
63
60
  }
64
- /** Crux summarizer backed by OpenRouter's OpenAI-compatible chat API. */
65
- export class OpenRouterCruxSummarizer {
66
- client;
61
+ /** Crux summarizer backed by any {@link ChatModel} via forced tool calling. */
62
+ export class ChatCruxSummarizer {
67
63
  model;
68
- constructor(apiKey, model, baseUrl = "https://openrouter.ai/api/v1") {
69
- this.client = new OpenAI({
70
- apiKey,
71
- baseURL: baseUrl,
72
- defaultHeaders: { "X-Title": "Context Graph Engine" },
73
- });
64
+ constructor(model) {
74
65
  this.model = model;
75
66
  }
76
67
  async describeFile(input) {
77
68
  if (input.nodes.length === 0)
78
69
  return [];
79
- const response = await this.client.chat.completions.create({
80
- model: this.model,
70
+ const res = await this.model.create({
81
71
  temperature: 0,
82
- response_format: { type: "json_object" },
72
+ maxTokens: 8192,
73
+ tools: [
74
+ {
75
+ name: RECORD_TOOL,
76
+ description: "Record each target definition's purpose and crux line range.",
77
+ parameters: SYMBOLS_SCHEMA,
78
+ },
79
+ ],
80
+ responseFormat: { kind: "tool", name: RECORD_TOOL },
83
81
  messages: [
84
82
  { role: "system", content: SYSTEM_PROMPT },
85
83
  { role: "user", content: userContent(input) },
86
84
  ],
87
85
  });
88
- return parseResults(response.choices[0]?.message?.content ?? "");
86
+ return parseResults(res.toolCalls[0]?.args);
89
87
  }
90
88
  }
91
89
  //# sourceMappingURL=crux.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"crux.js","sourceRoot":"","sources":["../../src/ai/crux.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"crux.js","sourceRoot":"","sources":["../../src/ai/crux.ts"],"names":[],"mappings":"AA6CA,MAAM,aAAa,GAAG;;;;;;;;qNAQ+L,CAAC;AAEtN,MAAM,WAAW,GAAG,gBAAgB,CAAC;AAErC,MAAM,cAAc,GAAG;IACrB,IAAI,EAAE,QAAQ;IACd,UAAU,EAAE;QACV,OAAO,EAAE;YACP,IAAI,EAAE,OAAO;YACb,KAAK,EAAE;gBACL,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE;oBACV,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;oBACtB,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;oBAC3B,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;oBAC9B,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;iBAC7B;gBACD,QAAQ,EAAE,CAAC,IAAI,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,CAAC;aACtD;SACF;KACF;IACD,QAAQ,EAAE,CAAC,SAAS,CAAC;CACb,CAAC;AAEX,kFAAkF;AAClF,MAAM,cAAc,GAAG,MAAM,CAAC;AAE9B,SAAS,WAAW,CAAC,MAAc;IACjC,MAAM,OAAO,GACX,MAAM,CAAC,MAAM,GAAG,cAAc,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,cAAc,CAAC,iBAAiB,CAAC,CAAC,CAAC,MAAM,CAAC;IAChG,OAAO,OAAO;SACX,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC;SACrC,IAAI,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC;AAED,SAAS,WAAW,CAAC,KAAoB;IACvC,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK;SACxB,GAAG,CACF,CAAC,CAAC,EAAE,EAAE,CACJ,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,IAAI,aAAa,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC,OAAO,EAAE;QAChE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAC3C;SACA,IAAI,CAAC,IAAI,CAAC,CAAC;IACd,OAAO,SAAS,KAAK,CAAC,IAAI,OAAO,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,iBAAiB,OAAO,EAAE,CAAC;AACvF,CAAC;AAED,gFAAgF;AAChF,SAAS,YAAY,CAAC,GAAsC;IAC1D,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC;QAAE,OAAO,EAAE,CAAC;IACnD,MAAM,GAAG,GAAG,CAAC,CAAU,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9F,OAAO,GAAG,CAAC,OAAO;SACf,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAA4B,CAAC;SACxC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,EAAE,KAAK,QAAQ,CAAC;SACvC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACX,EAAE,EAAE,CAAC,CAAC,EAAY;QAClB,OAAO,EAAE,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE;QAC9D,UAAU,EAAE,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC;QAC7B,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC;KAC1B,CAAC,CAAC,CAAC;AACR,CAAC;AAED,+EAA+E;AAC/E,MAAM,OAAO,kBAAkB;IACT;IAApB,YAAoB,KAAgB;QAAhB,UAAK,GAAL,KAAK,CAAW;IAAG,CAAC;IAExC,KAAK,CAAC,YAAY,CAAC,KAAoB;QACrC,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QACxC,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;YAClC,WAAW,EAAE,CAAC;YACd,SAAS,EAAE,IAAI;YACf,KAAK,EAAE;gBACL;oBACE,IAAI,EAAE,WAAW;oBACjB,WAAW,EAAE,8DAA8D;oBAC3E,UAAU,EAAE,cAAoD;iBACjE;aACF;YACD,cAAc,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE;YACnD,QAAQ,EAAE;gBACR,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,aAAa,EAAE;gBAC1C,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,CAAC,KAAK,CAAC,EAAE;aAC9C;SACF,CAAC,CAAC;QACH,OAAO,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,IAAyC,CAAC,CAAC;IACnF,CAAC;CACF"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Native Anthropic transport. Wraps `@anthropic-ai/sdk` (the Messages API), so
3
+ * Claude is a first-class provider rather than something tunneled through an
4
+ * OpenAI-compatible endpoint.
5
+ *
6
+ * The Messages API differs from Chat Completions in ways this adapter absorbs so
7
+ * the rest of graft never sees them:
8
+ * - `system` is a top-level parameter, not a message.
9
+ * - tool results ride inside a USER turn, and all results answering one
10
+ * assistant turn must be coalesced into a single user message.
11
+ * - `max_tokens` is required (callers get a default).
12
+ * - `temperature` is rejected by current models, so it is never forwarded.
13
+ * - structured output is a forced tool (there is no `response_format`).
14
+ * - assistant turns are replayed verbatim (via `providerRaw`) so thinking-block
15
+ * signatures survive a multi-turn loop.
16
+ */
17
+ import Anthropic from "@anthropic-ai/sdk";
18
+ import type { ChatModel, ChatRequest, ChatResponse } from "./types.js";
19
+ export interface AnthropicChatModelOptions {
20
+ apiKey: string;
21
+ model: string;
22
+ baseUrl?: string;
23
+ label?: string;
24
+ /** Inject a pre-built client (tests pass a stub; production omits it). */
25
+ client?: Anthropic;
26
+ }
27
+ export declare class AnthropicChatModel implements ChatModel {
28
+ readonly label: string;
29
+ private client;
30
+ private model;
31
+ constructor(opts: AnthropicChatModelOptions);
32
+ create(req: ChatRequest): Promise<ChatResponse>;
33
+ /** Reconstruct an assistant turn, replaying the original blocks when we made them. */
34
+ private assistantParam;
35
+ private fromResponse;
36
+ }
37
+ //# sourceMappingURL=anthropic.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anthropic.d.ts","sourceRoot":"","sources":["../../../src/ai/llm/anthropic.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,SAAS,MAAM,mBAAmB,CAAC;AAC1C,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAsC,MAAM,YAAY,CAAC;AAM3G,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB;AAKD,qBAAa,kBAAmB,YAAW,SAAS;IAClD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,KAAK,CAAS;gBAEV,IAAI,EAAE,yBAAyB;IAMrC,MAAM,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IAuDrD,sFAAsF;IACtF,OAAO,CAAC,cAAc;IAUtB,OAAO,CAAC,YAAY;CA4BrB"}
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Native Anthropic transport. Wraps `@anthropic-ai/sdk` (the Messages API), so
3
+ * Claude is a first-class provider rather than something tunneled through an
4
+ * OpenAI-compatible endpoint.
5
+ *
6
+ * The Messages API differs from Chat Completions in ways this adapter absorbs so
7
+ * the rest of graft never sees them:
8
+ * - `system` is a top-level parameter, not a message.
9
+ * - tool results ride inside a USER turn, and all results answering one
10
+ * assistant turn must be coalesced into a single user message.
11
+ * - `max_tokens` is required (callers get a default).
12
+ * - `temperature` is rejected by current models, so it is never forwarded.
13
+ * - structured output is a forced tool (there is no `response_format`).
14
+ * - assistant turns are replayed verbatim (via `providerRaw`) so thinking-block
15
+ * signatures survive a multi-turn loop.
16
+ */
17
+ import Anthropic from "@anthropic-ai/sdk";
18
+ const PROVIDER = "anthropic";
19
+ const JSON_TOOL = "emit_json";
20
+ const DEFAULT_MAX_TOKENS = 4096;
21
+ const cc = (on) => (on ? { cache_control: { type: "ephemeral" } } : {});
22
+ export class AnthropicChatModel {
23
+ label;
24
+ client;
25
+ model;
26
+ constructor(opts) {
27
+ this.model = opts.model;
28
+ this.label = opts.label ?? `${PROVIDER}:${opts.model}`;
29
+ this.client = opts.client ?? new Anthropic({ apiKey: opts.apiKey, baseURL: opts.baseUrl });
30
+ }
31
+ async create(req) {
32
+ const system = [];
33
+ const messages = [];
34
+ for (const m of req.messages) {
35
+ if (m.role === "system") {
36
+ system.push({ type: "text", text: m.content, ...cc(m.cacheBreakpoint) });
37
+ }
38
+ else if (m.role === "user") {
39
+ messages.push({ role: "user", content: [{ type: "text", text: m.content, ...cc(m.cacheBreakpoint) }] });
40
+ }
41
+ else if (m.role === "tool") {
42
+ // Tool results live in a user turn; coalesce consecutive results together.
43
+ const block = {
44
+ type: "tool_result",
45
+ tool_use_id: m.toolCallId ?? "",
46
+ content: [{ type: "text", text: m.content }],
47
+ ...cc(m.cacheBreakpoint),
48
+ };
49
+ const last = messages[messages.length - 1];
50
+ if (last?.role === "user" && Array.isArray(last.content)) {
51
+ last.content.push(block);
52
+ }
53
+ else {
54
+ messages.push({ role: "user", content: [block] });
55
+ }
56
+ }
57
+ else {
58
+ messages.push(this.assistantParam(m));
59
+ }
60
+ }
61
+ const tools = req.tools?.map(toAnthropicTool);
62
+ const params = {
63
+ model: this.model,
64
+ max_tokens: req.maxTokens ?? DEFAULT_MAX_TOKENS,
65
+ messages,
66
+ ...(system.length ? { system } : {}),
67
+ };
68
+ // temperature is intentionally NOT forwarded — current models reject it.
69
+ const fmt = req.responseFormat ?? { kind: "text" };
70
+ if (fmt.kind === "json") {
71
+ params.tools = [
72
+ ...(tools ?? []),
73
+ { name: JSON_TOOL, description: "Return the answer as a JSON object.", input_schema: { type: "object" } },
74
+ ];
75
+ params.tool_choice = { type: "tool", name: JSON_TOOL };
76
+ }
77
+ else if (fmt.kind === "tool") {
78
+ params.tools = tools;
79
+ params.tool_choice = { type: "tool", name: fmt.name };
80
+ }
81
+ else if (tools) {
82
+ params.tools = tools;
83
+ }
84
+ const resp = await this.client.messages.create(params);
85
+ return this.fromResponse(resp, fmt.kind);
86
+ }
87
+ /** Reconstruct an assistant turn, replaying the original blocks when we made them. */
88
+ assistantParam(m) {
89
+ if (m.providerRaw?.provider === PROVIDER)
90
+ return m.providerRaw.raw;
91
+ const content = [];
92
+ if (m.content)
93
+ content.push({ type: "text", text: m.content });
94
+ for (const tc of m.toolCalls ?? []) {
95
+ content.push({ type: "tool_use", id: tc.id, name: tc.name, input: tc.args });
96
+ }
97
+ return { role: "assistant", content };
98
+ }
99
+ fromResponse(resp, format) {
100
+ let text = "";
101
+ let toolCalls = [];
102
+ for (const block of resp.content) {
103
+ if (block.type === "text")
104
+ text += block.text;
105
+ else if (block.type === "tool_use")
106
+ toolCalls.push({ id: block.id, name: block.name, args: block.input });
107
+ }
108
+ if (format === "json") {
109
+ const jsonCall = toolCalls.find((c) => c.name === JSON_TOOL);
110
+ if (jsonCall)
111
+ text = JSON.stringify(jsonCall.args);
112
+ toolCalls = toolCalls.filter((c) => c.name !== JSON_TOOL);
113
+ }
114
+ return {
115
+ text,
116
+ toolCalls,
117
+ usage: normalizeUsage(resp.usage),
118
+ stopReason: resp.stop_reason ?? null,
119
+ // Replay the raw content verbatim so thinking-block signatures survive.
120
+ assistant: {
121
+ role: "assistant",
122
+ content: text,
123
+ toolCalls: toolCalls.length ? toolCalls : undefined,
124
+ providerRaw: { provider: PROVIDER, raw: { role: "assistant", content: resp.content } },
125
+ },
126
+ };
127
+ }
128
+ }
129
+ function toAnthropicTool(t) {
130
+ return { name: t.name, description: t.description, input_schema: t.parameters };
131
+ }
132
+ /** Anthropic reports uncached input in `input_tokens` and cache tokens separately. */
133
+ function normalizeUsage(u) {
134
+ return {
135
+ input: u.input_tokens ?? 0,
136
+ output: u.output_tokens ?? 0,
137
+ cacheRead: u.cache_read_input_tokens ?? 0,
138
+ cacheCreate: u.cache_creation_input_tokens ?? 0,
139
+ };
140
+ }
141
+ //# sourceMappingURL=anthropic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anthropic.js","sourceRoot":"","sources":["../../../src/ai/llm/anthropic.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * One place that turns resolved config into a {@link ChatModel}. `provider` names
3
+ * the WIRE FORMAT, not a vendor: `openai` speaks the OpenAI-compatible API (point
4
+ * `baseUrl` at OpenRouter, Fireworks, a LiteLLM proxy, Groq, a local server, …),
5
+ * `anthropic` speaks the native Messages API. Adding a vendor is a base URL, not
6
+ * a code change; adding a wire format is one new adapter here.
7
+ */
8
+ import type { ChatModel } from "./types.js";
9
+ export type ProviderKind = "openai" | "anthropic";
10
+ export interface ChatModelConfig {
11
+ provider: ProviderKind;
12
+ apiKey: string;
13
+ model: string;
14
+ baseUrl?: string;
15
+ /** Extra default headers for OpenAI-compatible endpoints (e.g. OpenRouter `X-Title`). */
16
+ headers?: Record<string, string>;
17
+ }
18
+ export declare function createChatModel(cfg: ChatModelConfig): ChatModel;
19
+ //# sourceMappingURL=factory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../../src/ai/llm/factory.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAI5C,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,WAAW,CAAC;AAElD,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,YAAY,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yFAAyF;IACzF,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,eAAe,GAAG,SAAS,CAgB/D"}
@@ -0,0 +1,20 @@
1
+ import { OpenAIChatModel } from "./openai.js";
2
+ import { AnthropicChatModel } from "./anthropic.js";
3
+ export function createChatModel(cfg) {
4
+ switch (cfg.provider) {
5
+ case "anthropic":
6
+ return new AnthropicChatModel({ apiKey: cfg.apiKey, model: cfg.model, baseUrl: cfg.baseUrl });
7
+ case "openai":
8
+ return new OpenAIChatModel({
9
+ apiKey: cfg.apiKey,
10
+ model: cfg.model,
11
+ baseUrl: cfg.baseUrl,
12
+ headers: cfg.headers,
13
+ });
14
+ default: {
15
+ const _exhaustive = cfg.provider;
16
+ throw new Error(`unknown provider: ${String(_exhaustive)}`);
17
+ }
18
+ }
19
+ }
20
+ //# sourceMappingURL=factory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factory.js","sourceRoot":"","sources":["../../../src/ai/llm/factory.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAapD,MAAM,UAAU,eAAe,CAAC,GAAoB;IAClD,QAAQ,GAAG,CAAC,QAAQ,EAAE,CAAC;QACrB,KAAK,WAAW;YACd,OAAO,IAAI,kBAAkB,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;QAChG,KAAK,QAAQ;YACX,OAAO,IAAI,eAAe,CAAC;gBACzB,MAAM,EAAE,GAAG,CAAC,MAAM;gBAClB,KAAK,EAAE,GAAG,CAAC,KAAK;gBAChB,OAAO,EAAE,GAAG,CAAC,OAAO;gBACpB,OAAO,EAAE,GAAG,CAAC,OAAO;aACrB,CAAC,CAAC;QACL,OAAO,CAAC,CAAC,CAAC;YACR,MAAM,WAAW,GAAU,GAAG,CAAC,QAAQ,CAAC;YACxC,MAAM,IAAI,KAAK,CAAC,qBAAqB,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;QAC9D,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * OpenAI-compatible transport. Wraps the `openai` SDK pointed at any
3
+ * OpenAI-compatible endpoint (OpenAI, OpenRouter, Fireworks, a LiteLLM proxy,
4
+ * Groq, Together, DeepSeek, a local server, …) — the user picks the endpoint
5
+ * with `baseUrl` and authenticates with their own key.
6
+ *
7
+ * This adapter reproduces graft's historical wire behavior exactly: temperature
8
+ * is forwarded, cache breakpoints become `cache_control` content parts (which
9
+ * OpenRouter forwards to Anthropic), and cached tokens are subtracted out of the
10
+ * input count so {@link Usage.input} is uncached-only.
11
+ */
12
+ import OpenAI from "openai";
13
+ import type { ChatModel, ChatRequest, ChatResponse } from "./types.js";
14
+ export interface OpenAIChatModelOptions {
15
+ apiKey: string;
16
+ model: string;
17
+ baseUrl?: string;
18
+ /** Stable manifest label; defaults to `openai:<model>`. */
19
+ label?: string;
20
+ /** Extra default headers (e.g. OpenRouter's `X-Title`). */
21
+ headers?: Record<string, string>;
22
+ /** Inject a pre-built client (tests pass a stub; production omits it). */
23
+ client?: OpenAI;
24
+ }
25
+ export declare class OpenAIChatModel implements ChatModel {
26
+ readonly label: string;
27
+ private client;
28
+ private model;
29
+ constructor(opts: OpenAIChatModelOptions);
30
+ create(req: ChatRequest): Promise<ChatResponse>;
31
+ private fromResponse;
32
+ }
33
+ //# sourceMappingURL=openai.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai.d.ts","sourceRoot":"","sources":["../../../src/ai/llm/openai.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAsC,MAAM,YAAY,CAAC;AAM3G,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AA4CD,qBAAa,eAAgB,YAAW,SAAS;IAC/C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAAS;gBAEV,IAAI,EAAE,sBAAsB;IAYlC,MAAM,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IA2BrD,OAAO,CAAC,YAAY;CA6CrB"}