agenticweb-cli 0.1.1 → 0.2.0

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
@@ -40,10 +40,25 @@ npx agenticweb-cli serve --edge
40
40
  ```
41
41
 
42
42
  - **init** starts a project and asks you to prove you own the site (a DNS record or a small file).
43
- - **build** reads your site and pulls out what matters: pages, products, menus, hours, FAQs and policies. With an `ANTHROPIC_API_KEY` it uses Claude. Without one, built-in rules still do a good job.
43
+ - **build** reads your site and pulls out what matters: pages, products, menus, hours, FAQs and policies.
44
44
  - **review** opens a page where you choose what agents can see and which actions they can use. Everything starts off.
45
45
  - **serve --edge** shows your site with the agent version added, at `http://127.0.0.1:8787`.
46
46
 
47
+ ## Do I need an AI key?
48
+
49
+ **No.** AgenticWeb works fully without one. The benchmark above was run with no AI key at all.
50
+
51
+ **What an AI provider is for:** it's the AI that AgenticWeb can call on for a few optional extras. The core work never depends on it: building the agent version, the safety checks, bookings and checkout all run on AgenticWeb's own rules.
52
+
53
+ | | Without a key | With a key |
54
+ |---|---|---|
55
+ | Reading your site | Built-in rules, good for well-built sites | Smarter on messy pages (and PDFs, with Claude) |
56
+ | Answering agents' questions | The matching facts, with sources | A short written answer, with sources |
57
+ | Testing your site as an agent | Quick checks with set answers | A real AI agent tries each task |
58
+ | Questions your site can't answer yet | The question, for you to answer | A draft answer from your site's own facts |
59
+
60
+ **Pick any provider:** Claude (the default), OpenAI, DeepSeek, Gemini, or a free model running on your own computer (Ollama or LM Studio). It's one line in the config, and the [guide](docs/GUIDE.md#choosing-an-ai-provider) shows how. Your key stays on your computer or server, and requests go straight to the provider you chose, never to us.
61
+
47
62
  ## Put it on your site
48
63
 
49
64
  **Recommended: edge mode.** AgenticWeb runs in front of your site on your own domain. Nothing gets installed on your site, so it works with WordPress, Shopify themes, Webflow, custom code or anything else.
@@ -62,15 +77,15 @@ This prepares a Cloudflare Worker for your domain. Not on Cloudflare? Use `--tar
62
77
  | Markdown pages | Any page, as clean text, when an agent asks for it |
63
78
  | Product and business data | Prices, stock, hours and FAQs in the standard schema.org format |
64
79
  | `/.agent/ask` | Ask a question in plain words and get an answer with sources |
65
- | MCP server | Your site as tools for Claude and other AI apps |
80
+ | MCP server | Your site as tools for ChatGPT, Claude, Gemini, Cursor and other AI apps |
66
81
  | WebMCP | The same tools for AI agents built into browsers |
67
82
  | UCP and ACP checkout | The checkout protocols shopping agents use, for any store, not only Shopify |
68
83
 
69
84
  ## Check that it works
70
85
 
71
- - **Test it like an agent.** `agenticweb test` runs your real tasks, like "book a table for 4 on Friday", as an AI agent on both your site and the agent version. You get an **Agent Readiness score**, a badge, and a warning when a task that used to work breaks.
86
+ - **Test it like an agent.** `npx agenticweb-cli test` tries your real tasks, like "book a table for 4 on Friday", on both your site and the agent version (with an AI key, a real AI agent does them). You get an **Agent Readiness score**, a badge, and a warning when a task that used to work breaks.
72
87
  - **Prove it's the same story.** Every build checks each fact (prices, hours, contact details) against your live pages and signs the result with your key. Anyone can check it with `npx agenticweb-cli check <agent-branch-url>`.
73
- - **Fill the gaps.** `agenticweb suggest` turns questions agents asked that your site couldn't answer into drafted FAQ answers. You approve them and add them to your site, and agents pick them up from there.
88
+ - **Fill the gaps.** `npx agenticweb-cli suggest` lists questions agents asked that your site couldn't answer, with a draft answer for each if you have an AI key. You approve them and add them to your site, and agents pick them up from there.
74
89
 
75
90
  ## Works with
76
91
 
@@ -88,10 +103,11 @@ Shopify, WooCommerce, Stripe, Cal.com, any API with an OpenAPI description, any
88
103
 
89
104
  More in [SECURITY.md](SECURITY.md).
90
105
 
91
- ## Use it from Claude
106
+ ## Use it from your AI assistant
92
107
 
93
- - **Claude Code:** run `/plugin marketplace add adibfarah/agenticweb`, then `/agentify https://mysite.com`, and Claude walks you through each step.
94
- - **Claude Desktop or any MCP app:** add an MCP server with the command `npx` and the arguments `["agenticweb-cli@latest", "mcp"]`.
108
+ - **Any coding assistant** (Claude Code, Codex, Cursor, Gemini CLI and others) can run the `npx agenticweb-cli` commands for you.
109
+ - **Any MCP app** (ChatGPT, Claude Desktop, Cursor, VS Code and others): add an MCP server with the command `npx` and the arguments `["agenticweb-cli@latest", "mcp"]`.
110
+ - **Claude Code plugin:** run `/plugin marketplace add adibfarah/agenticweb`, then `/agentify https://mysite.com` for a guided walkthrough.
95
111
 
96
112
  ## Learn more
97
113
 
@@ -20788,6 +20788,122 @@ var init_util2 = __esm({
20788
20788
  }
20789
20789
  });
20790
20790
 
20791
+ // packages/core/src/llm-providers.ts
20792
+ function resolveLlm(s, env) {
20793
+ if (!s.enabled) return void 0;
20794
+ const info = PROVIDERS[s.provider];
20795
+ const keyEnv = s.apiKeyEnv ?? info.apiKeyEnv;
20796
+ const apiKey = env[keyEnv] || void 0;
20797
+ if (s.provider === "anthropic") {
20798
+ if (!apiKey && !env.ANTHROPIC_AUTH_TOKEN && !env.ANTHROPIC_PROFILE) return void 0;
20799
+ } else if (!apiKey && !(s.provider === "openai-compatible" && s.baseUrl)) {
20800
+ return void 0;
20801
+ }
20802
+ const baseUrl = s.baseUrl ?? info.baseUrl;
20803
+ if (s.provider === "openai-compatible" && !baseUrl) throw new Error('llm.provider is "openai-compatible": set llm.baseUrl, e.g. "http://localhost:11434/v1".');
20804
+ return {
20805
+ provider: s.provider,
20806
+ label: info.label,
20807
+ apiKey,
20808
+ baseUrl,
20809
+ jsonMode: info.jsonMode,
20810
+ maxTokensParam: info.maxTokensParam,
20811
+ model: (configured) => {
20812
+ if (s.model) return s.model;
20813
+ if (s.provider === "anthropic") return configured || info.defaultModel;
20814
+ if (configured && !configured.startsWith("claude-")) return configured;
20815
+ if (!info.defaultModel) throw new Error(`Set llm.model to a model your ${info.label} serves.`);
20816
+ return info.defaultModel;
20817
+ }
20818
+ };
20819
+ }
20820
+ async function chatCompletion(r, body, f = fetch) {
20821
+ if (!r.baseUrl) throw new Error(`${r.label} has no API address.`);
20822
+ const res = await f(`${r.baseUrl.replace(/\/+$/, "")}/chat/completions`, {
20823
+ method: "POST",
20824
+ headers: { "content-type": "application/json", ...r.apiKey ? { authorization: `Bearer ${r.apiKey}` } : {} },
20825
+ body: JSON.stringify(body)
20826
+ });
20827
+ if (!res.ok) throw new Error(`${r.label} returned HTTP ${res.status}: ${(await res.text()).slice(0, 300)}`);
20828
+ return await res.json();
20829
+ }
20830
+ function jsonSchemaOf(schema) {
20831
+ const clean = (v) => {
20832
+ if (Array.isArray(v)) return v.map(clean);
20833
+ if (!v || typeof v !== "object") return v;
20834
+ const out = {};
20835
+ for (const [k, val] of Object.entries(v)) {
20836
+ if (k === "$schema") continue;
20837
+ if (k === "type" && Array.isArray(val)) {
20838
+ const types = val.filter((t) => t !== "null");
20839
+ out.type = types[0] ?? "string";
20840
+ if (types.length < val.length) out.nullable = true;
20841
+ continue;
20842
+ }
20843
+ out[k] = clean(val);
20844
+ }
20845
+ return out;
20846
+ };
20847
+ return clean(external_exports.toJSONSchema(schema, { unrepresentable: "any" }));
20848
+ }
20849
+ async function extractJson(r, args, f = fetch) {
20850
+ const schemaJson = jsonSchemaOf(args.schema);
20851
+ const system = r.jsonMode === "schema" ? args.system : `${args.system}
20852
+
20853
+ Reply with one JSON object only, matching this JSON Schema:
20854
+ ${JSON.stringify(schemaJson)}`;
20855
+ const messages = [
20856
+ { role: "system", content: system },
20857
+ { role: "user", content: args.content }
20858
+ ];
20859
+ const format = r.jsonMode === "schema" ? { type: "json_schema", json_schema: { name: "result", schema: schemaJson, strict: false } } : { type: "json_object" };
20860
+ let problem = "";
20861
+ for (let attempt = 0; attempt < 2; attempt++) {
20862
+ const res = await chatCompletion(r, { model: r.model(args.model), messages, response_format: format, ...maxTokens(r, args.maxTokens ?? 8e3) }, f);
20863
+ const choice = res.choices[0];
20864
+ if (!choice) throw new Error(`${r.label} returned no answer.`);
20865
+ if (choice.message.refusal || choice.finish_reason === "content_filter") throw new Error(`${r.label} declined to process this content.`);
20866
+ const text2 = choice.message.content ?? "";
20867
+ let value;
20868
+ try {
20869
+ value = parseJson(text2);
20870
+ } catch {
20871
+ problem = "The reply wasn't valid JSON.";
20872
+ messages.push({ role: "assistant", content: text2 }, { role: "user", content: `${problem} Reply with the JSON object only.` });
20873
+ continue;
20874
+ }
20875
+ const parsed = args.schema.safeParse(value);
20876
+ if (parsed.success) return parsed.data;
20877
+ problem = parsed.error.issues.slice(0, 5).map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
20878
+ messages.push({ role: "assistant", content: text2 }, { role: "user", content: `That JSON doesn't match the schema (${problem}). Reply with corrected JSON only.` });
20879
+ }
20880
+ throw new Error(`${r.label} didn't return valid data: ${problem}`);
20881
+ }
20882
+ var PROVIDER_IDS, PROVIDERS, maxTokens, parseJson;
20883
+ var init_llm_providers = __esm({
20884
+ "packages/core/src/llm-providers.ts"() {
20885
+ "use strict";
20886
+ init_zod();
20887
+ PROVIDER_IDS = ["anthropic", "openai", "deepseek", "gemini", "openai-compatible"];
20888
+ PROVIDERS = {
20889
+ anthropic: { label: "Claude", apiKeyEnv: "ANTHROPIC_API_KEY", defaultModel: "claude-opus-5-5", jsonMode: "schema", maxTokensParam: "max_tokens" },
20890
+ openai: { label: "OpenAI", baseUrl: "https://api.openai.com/v1", apiKeyEnv: "OPENAI_API_KEY", defaultModel: "gpt-6.1-sol", jsonMode: "schema", maxTokensParam: "max_completion_tokens" },
20891
+ deepseek: { label: "DeepSeek", baseUrl: "https://api.deepseek.com", apiKeyEnv: "DEEPSEEK_API_KEY", defaultModel: "deepseek-flash", jsonMode: "object", maxTokensParam: "max_tokens" },
20892
+ gemini: {
20893
+ label: "Gemini",
20894
+ baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
20895
+ apiKeyEnv: "GEMINI_API_KEY",
20896
+ defaultModel: "gemini-3-flash-preview",
20897
+ jsonMode: "schema",
20898
+ maxTokensParam: "max_tokens"
20899
+ },
20900
+ "openai-compatible": { label: "OpenAI-compatible API", apiKeyEnv: "LLM_API_KEY", defaultModel: "", jsonMode: "object", maxTokensParam: "max_tokens" }
20901
+ };
20902
+ maxTokens = (r, n) => ({ [r.maxTokensParam]: n });
20903
+ parseJson = (text2) => JSON.parse(text2.trim().replace(/^```(?:json)?\s*/i, "").replace(/```\s*$/, ""));
20904
+ }
20905
+ });
20906
+
20791
20907
  // packages/core/src/config.ts
20792
20908
  var ActionOverride, crawlDefaults, llmDefaults, edgeDefaults, testsDefaults, commerceDefaults, ProjectConfig;
20793
20909
  var init_config = __esm({
@@ -20795,6 +20911,7 @@ var init_config = __esm({
20795
20911
  "use strict";
20796
20912
  init_zod();
20797
20913
  init_schema();
20914
+ init_llm_providers();
20798
20915
  ActionOverride = external_exports.object({
20799
20916
  name: external_exports.string(),
20800
20917
  enabled: external_exports.boolean().optional(),
@@ -20810,6 +20927,7 @@ var init_config = __esm({
20810
20927
  crawlDefaults = { maxPages: 200, include: [], exclude: [], render: "fetch", respectRobots: true, concurrency: 4 };
20811
20928
  llmDefaults = {
20812
20929
  enabled: true,
20930
+ provider: "anthropic",
20813
20931
  classifyModel: "claude-opus-5-5",
20814
20932
  extractModel: "claude-opus-5-5",
20815
20933
  actionsModel: "claude-opus-5-5",
@@ -20851,6 +20969,14 @@ var init_config = __esm({
20851
20969
  actions: external_exports.array(ActionOverride).default([]),
20852
20970
  llm: external_exports.object({
20853
20971
  enabled: external_exports.boolean().default(true),
20972
+ /** Which AI to use: Claude (default), OpenAI, DeepSeek, Gemini, or any OpenAI-compatible API (including local ones). */
20973
+ provider: external_exports.enum(PROVIDER_IDS).default("anthropic"),
20974
+ /** API address for "openai-compatible", e.g. http://localhost:11434/v1 for Ollama. */
20975
+ baseUrl: external_exports.string().url().optional(),
20976
+ /** Env var holding the API key, if not the provider's usual one (ANTHROPIC_API_KEY, OPENAI_API_KEY, DEEPSEEK_API_KEY, GEMINI_API_KEY, LLM_API_KEY). */
20977
+ apiKeyEnv: external_exports.string().optional(),
20978
+ /** One model for every step. Without it, non-Claude providers use their own default model. */
20979
+ model: external_exports.string().optional(),
20854
20980
  classifyModel: external_exports.string().default(llmDefaults.classifyModel),
20855
20981
  extractModel: external_exports.string().default(llmDefaults.extractModel),
20856
20982
  actionsModel: external_exports.string().default(llmDefaults.actionsModel),
@@ -20951,6 +21077,7 @@ var init_src = __esm({
20951
21077
  init_fields();
20952
21078
  init_util2();
20953
21079
  init_config();
21080
+ init_llm_providers();
20954
21081
  }
20955
21082
  });
20956
21083
 
@@ -90044,9 +90171,9 @@ var init_client = __esm({
90044
90171
  }
90045
90172
  return url2.toString();
90046
90173
  }
90047
- _calculateNonstreamingTimeout(maxTokens) {
90174
+ _calculateNonstreamingTimeout(maxTokens2) {
90048
90175
  const defaultTimeout = 10 * 60;
90049
- const expectedTimeout = 60 * 60 * maxTokens / 128e3;
90176
+ const expectedTimeout = 60 * 60 * maxTokens2 / 128e3;
90050
90177
  if (expectedTimeout > defaultTimeout) {
90051
90178
  throw new AnthropicError("Streaming is required for operations that may take longer than 10 minutes. See https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/typescript#streaming-responses for more details");
90052
90179
  }
@@ -90396,11 +90523,11 @@ var init_client = __esm({
90396
90523
  const jitter2 = 1 - Math.random() * 0.25;
90397
90524
  return sleepSeconds * jitter2 * 1e3;
90398
90525
  }
90399
- calculateNonstreamingTimeout(maxTokens, maxNonstreamingTokens) {
90526
+ calculateNonstreamingTimeout(maxTokens2, maxNonstreamingTokens) {
90400
90527
  const maxTime = 60 * 60 * 1e3;
90401
90528
  const defaultTime = 60 * 10 * 1e3;
90402
- const expectedTime = maxTime * maxTokens / 128e3;
90403
- if (expectedTime > defaultTime || maxNonstreamingTokens != null && maxTokens > maxNonstreamingTokens) {
90529
+ const expectedTime = maxTime * maxTokens2 / 128e3;
90530
+ if (expectedTime > defaultTime || maxNonstreamingTokens != null && maxTokens2 > maxNonstreamingTokens) {
90404
90531
  throw new AnthropicError("Streaming is required for operations that may take longer than 10 minutes. See https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/typescript#long-requests for more details");
90405
90532
  }
90406
90533
  return defaultTime;
@@ -91266,15 +91393,26 @@ var init_zod2 = __esm({
91266
91393
  });
91267
91394
 
91268
91395
  // packages/understand/src/llm.ts
91269
- function hasClaudeCredentials(env = process.env) {
91270
- return Boolean(env.ANTHROPIC_API_KEY || env.ANTHROPIC_AUTH_TOKEN || env.ANTHROPIC_PROFILE);
91396
+ function llmFromSettings(s, env = process.env, f) {
91397
+ const r = resolveLlm(s, env);
91398
+ if (!r) return void 0;
91399
+ if (r.provider === "anthropic") {
91400
+ const claude = new ClaudeLlm(r.apiKey ? new Anthropic({ apiKey: r.apiKey }) : void 0);
91401
+ const llm = {
91402
+ extract: (a) => claude.extract({ ...a, model: r.model(a.model) }),
91403
+ readDocument: (a) => claude.readDocument({ ...a, model: r.model(a.model) })
91404
+ };
91405
+ return { llm, label: r.label, resolved: r };
91406
+ }
91407
+ return { llm: new ChatCompletionsLlm(r, f), label: r.label, resolved: r };
91271
91408
  }
91272
- var RefusalError, ClaudeLlm;
91409
+ var RefusalError, ClaudeLlm, ChatCompletionsLlm, apiKeyEnvFor;
91273
91410
  var init_llm = __esm({
91274
91411
  "packages/understand/src/llm.ts"() {
91275
91412
  "use strict";
91276
91413
  init_sdk();
91277
91414
  init_zod2();
91415
+ init_src();
91278
91416
  RefusalError = class extends Error {
91279
91417
  };
91280
91418
  ClaudeLlm = class {
@@ -91322,6 +91460,18 @@ var init_llm = __esm({
91322
91460
  return message.parsed_output;
91323
91461
  }
91324
91462
  };
91463
+ ChatCompletionsLlm = class {
91464
+ r;
91465
+ f;
91466
+ constructor(r, f) {
91467
+ this.r = r;
91468
+ this.f = f;
91469
+ }
91470
+ extract(args) {
91471
+ return extractJson(this.r, args, this.f);
91472
+ }
91473
+ };
91474
+ apiKeyEnvFor = (s) => s.apiKeyEnv ?? PROVIDERS[s.provider].apiKeyEnv;
91325
91475
  }
91326
91476
  });
91327
91477
 
@@ -116494,9 +116644,19 @@ async function createClaudeAsker(opts = {}) {
116494
116644
  }
116495
116645
  };
116496
116646
  }
116647
+ async function askerFor(s, env, f) {
116648
+ const r = resolveLlm(s, env);
116649
+ if (!r) return void 0;
116650
+ if (r.provider === "anthropic") {
116651
+ const claude = await createClaudeAsker({ apiKey: r.apiKey });
116652
+ return { label: r.label, extract: (a) => claude.extract({ ...a, model: r.model(a.model) }) };
116653
+ }
116654
+ return { label: r.label, extract: (a) => extractJson(r, a, f) };
116655
+ }
116497
116656
  var init_claude = __esm({
116498
116657
  "packages/runtime/src/claude.ts"() {
116499
116658
  "use strict";
116659
+ init_src();
116500
116660
  }
116501
116661
  });
116502
116662
 
@@ -116683,7 +116843,7 @@ async function cmdCi(project, opts, out = { log: (m) => console.log(m) }) {
116683
116843
  const cron = hours === 24 ? "17 3 * * *" : `17 */${hours} * * *`;
116684
116844
  const file3 = path6.join(project.dir, ".github", "workflows", "agenticweb-sync.yml");
116685
116845
  await mkdir4(path6.dirname(file3), { recursive: true });
116686
- await writeFile3(file3, WORKFLOW(cron, opts.target ?? config2.deploy.target));
116846
+ await writeFile3(file3, WORKFLOW(cron, opts.target ?? config2.deploy.target, config2.llm.apiKeyEnv ?? PROVIDERS[config2.llm.provider].apiKeyEnv));
116687
116847
  out.log(`Wrote ${path6.relative(project.dir, file3)}. It rebuilds every ${hours} hour(s).`);
116688
116848
  out.log("Add the repository secrets it uses (Settings \u2192 Secrets and variables \u2192 Actions), then commit the file.");
116689
116849
  }
@@ -116691,10 +116851,11 @@ var WORKFLOW;
116691
116851
  var init_sync = __esm({
116692
116852
  "packages/cli/src/sync.ts"() {
116693
116853
  "use strict";
116854
+ init_src();
116694
116855
  init_src7();
116695
116856
  init_project();
116696
116857
  init_commands();
116697
- WORKFLOW = (cron, target) => `# Keeps your agent branch in sync with your site. Generated by AgenticWeb.
116858
+ WORKFLOW = (cron, target, aiKeyEnv) => `# Keeps your agent branch in sync with your site. Generated by AgenticWeb.
116698
116859
  name: Sync agent branch
116699
116860
  on:
116700
116861
  schedule:
@@ -116712,14 +116873,14 @@ jobs:
116712
116873
  - name: Rebuild the agent branch
116713
116874
  run: npx agenticweb-cli@latest build
116714
116875
  env:
116715
- ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
116876
+ ${aiKeyEnv}: \${{ secrets.${aiKeyEnv} }}
116716
116877
  # The contents of .agenticweb/signing-key.json, so every build is signed with the same key.
116717
116878
  AGENTICWEB_SIGNING_KEY: \${{ secrets.AGENTICWEB_SIGNING_KEY }}
116718
116879
  - name: Agent Readiness tests (a task that used to pass and now fails stops the deploy)
116719
116880
  if: hashFiles('agenticweb.tests.json') != ''
116720
116881
  run: npx agenticweb-cli@latest test --strict
116721
116882
  env:
116722
- ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
116883
+ ${aiKeyEnv}: \${{ secrets.${aiKeyEnv} }}
116723
116884
  - name: Keep the readiness report
116724
116885
  if: always() && hashFiles('agent-readiness/report.md') != ''
116725
116886
  uses: actions/upload-artifact@v4
@@ -119109,6 +119270,7 @@ __export(readiness_exports, {
119109
119270
  TasksFile: () => TasksFile,
119110
119271
  badgeSvg: () => badgeSvg,
119111
119272
  basicChecks: () => basicChecks,
119273
+ chatCompletionsLoop: () => chatCompletionsLoop,
119112
119274
  claudeLoop: () => claudeLoop,
119113
119275
  cmdTest: () => cmdTest,
119114
119276
  crawlForAnswer: () => crawlForAnswer,
@@ -119266,7 +119428,7 @@ async function crawlForAnswer(site, expect, opts = {}) {
119266
119428
  }
119267
119429
  async function builtInAgentArm(task, branch) {
119268
119430
  if (task.action) {
119269
- if (!task.input) return { ran: false, success: false, requests: 0, tokens: 0, note: "needs Claude (set ANTHROPIC_API_KEY) or an `input` to run without it" };
119431
+ if (!task.input) return { ran: false, success: false, requests: 0, tokens: 0, note: "needs an AI key (see llm.provider) or an `input` to run without it" };
119270
119432
  const res2 = await branch.fetcher(
119271
119433
  new Request(`${branch.baseUrl}/api/${task.action}`, {
119272
119434
  method: "POST",
@@ -119343,14 +119505,44 @@ async function claudeLoop(args) {
119343
119505
  }
119344
119506
  return done("", `stopped after ${args.maxTurns} turns`);
119345
119507
  }
119346
- async function claudeAgentArm(task, ctx) {
119508
+ async function chatCompletionsLoop(args) {
119509
+ const messages = [
119510
+ { role: "system", content: args.system },
119511
+ { role: "user", content: args.task }
119512
+ ];
119513
+ const tools = args.tools.map((t) => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.input_schema } }));
119514
+ let tokens = 0;
119515
+ const calls = [];
119516
+ const results = [];
119517
+ const done = (answer, note) => ({ answer, requests: calls.length, tokens, calls, results, note });
119518
+ for (let turn = 0; turn < args.maxTurns; turn++) {
119519
+ const res = await chatCompletion(args.r, { model: args.model, messages, tools, ...maxTokens(args.r, 8e3) }, args.fetch);
119520
+ tokens += (res.usage?.prompt_tokens ?? 0) + (res.usage?.completion_tokens ?? 0);
119521
+ const choice = res.choices[0];
119522
+ if (!choice) return done("", `${args.r.label} returned no answer`);
119523
+ const msg = choice.message;
119524
+ if (msg.refusal || choice.finish_reason === "content_filter") return done(msg.content ?? "", `${args.r.label} declined the task`);
119525
+ messages.push({ role: "assistant", content: msg.content ?? null, ...msg.tool_calls?.length ? { tool_calls: msg.tool_calls } : {} });
119526
+ if (!msg.tool_calls?.length) return done((msg.content ?? "").trim(), choice.finish_reason === "length" ? "the answer was cut off" : void 0);
119527
+ for (const call of msg.tool_calls) {
119528
+ calls.push(call.function.name);
119529
+ let out;
119530
+ try {
119531
+ out = await args.run(call.function.name, call.function.arguments ? JSON.parse(call.function.arguments) : {});
119532
+ } catch (err) {
119533
+ out = { text: `Error: ${err.message}`, isError: true };
119534
+ }
119535
+ results.push({ name: call.function.name, status: out.status, isError: out.isError });
119536
+ messages.push({ role: "tool", tool_call_id: call.id, content: out.text.slice(0, 4e4) || "(empty)" });
119537
+ }
119538
+ }
119539
+ return done("", `stopped after ${args.maxTurns} turns`);
119540
+ }
119541
+ async function aiAgentArm(task, ctx) {
119347
119542
  const mcp = await connectMcp(ctx.branch);
119348
119543
  try {
119349
119544
  const tools = (await mcp.listTools()).map((t) => ({ name: t.name, description: t.description ?? "", input_schema: t.inputSchema }));
119350
- const r = await claudeLoop({
119351
- client: ctx.client,
119352
- model: ctx.model,
119353
- effort: ctx.effort,
119545
+ const r = await ctx.loop({
119354
119546
  maxTurns: ctx.maxTurns,
119355
119547
  system: SYSTEM(ctx.site, "agent"),
119356
119548
  task: task.task,
@@ -119372,13 +119564,10 @@ async function claudeAgentArm(task, ctx) {
119372
119564
  await mcp.close();
119373
119565
  }
119374
119566
  }
119375
- async function claudeHumanArm(task, ctx) {
119567
+ async function aiHumanArm(task, ctx) {
119376
119568
  const f = ctx.fetch ?? fetch;
119377
119569
  const origin = new URL(ctx.site);
119378
- const r = await claudeLoop({
119379
- client: ctx.client,
119380
- model: ctx.model,
119381
- effort: ctx.effort,
119570
+ const r = await ctx.loop({
119382
119571
  maxTurns: ctx.maxTurns,
119383
119572
  system: SYSTEM(ctx.site, "human"),
119384
119573
  task: task.task,
@@ -119428,7 +119617,7 @@ function reportMarkdown(r) {
119428
119617
  const lines = [
119429
119618
  `# Agent Readiness: ${r.score}/100`,
119430
119619
  "",
119431
- `${r.site}, tested ${r.ranAt.slice(0, 16).replace("T", " ")} UTC against ${r.target}. ${r.mode === "claude" ? `Tasks were done by Claude (${r.model}) using tools.` : "Built-in mode: no Claude. Lookups use /ask; actions run with the input in the tasks file."}`,
119620
+ `${r.site}, tested ${r.ranAt.slice(0, 16).replace("T", " ")} UTC against ${r.target}. ${r.mode === "ai" ? `Tasks were done by ${r.provider ?? "an AI agent"} (${r.model}) using tools.` : "Built-in mode: no AI. Lookups use /ask; actions run with the input in the tasks file."}`,
119432
119621
  "",
119433
119622
  `- **Tasks:** ${t.passed} of ${t.total} done on the agent branch${t.humanRan ? `, ${t.humanPassed} of ${t.humanRan} on the human site` : ""}.`,
119434
119623
  `- **Basics:** ${r.checks.filter((c) => c.ok).length} of ${r.checks.length} checks pass.`,
@@ -119493,24 +119682,38 @@ ${parsed.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n
119493
119682
  let tasks = parsed.data.tasks;
119494
119683
  if (opts.only) tasks = tasks.filter((t) => t.id === opts.only);
119495
119684
  if (!tasks.length) throw new UserError(opts.only ? `No task "${opts.only}" in ${TASKS_FILE}.` : `${TASKS_FILE} has no tasks.`);
119496
- const useClaude = !opts.noLlm && (Boolean(opts.client) || config2.llm.enabled && hasClaudeCredentials());
119497
- let client = opts.client;
119498
- if (useClaude && !client) {
119499
- const { default: Anthropic2 } = await Promise.resolve().then(() => (init_sdk(), sdk_exports));
119500
- client = new Anthropic2();
119501
- }
119502
- const llm = useClaude && !opts.client ? await createClaudeAsker() : void 0;
119685
+ const ai = opts.noLlm ? void 0 : opts.client ? { provider: "anthropic", label: "Claude" } : resolveLlm(config2.llm, process.env);
119686
+ const useAi = Boolean(ai);
119687
+ let loop;
119688
+ let model = config2.tests.model;
119689
+ if (ai && (ai.provider === "anthropic" || opts.client)) {
119690
+ let client = opts.client;
119691
+ if (!client) {
119692
+ const { default: Anthropic2 } = await Promise.resolve().then(() => (init_sdk(), sdk_exports));
119693
+ const r = ai;
119694
+ client = r.apiKey ? new Anthropic2({ apiKey: r.apiKey }) : new Anthropic2();
119695
+ model = r.model(config2.tests.model);
119696
+ }
119697
+ const c = client;
119698
+ const m = model;
119699
+ loop = (a) => claudeLoop({ ...a, client: c, model: m, effort: config2.tests.effort });
119700
+ } else if (ai) {
119701
+ const r = ai;
119702
+ model = r.model(config2.tests.model);
119703
+ const m = model;
119704
+ loop = (a) => chatCompletionsLoop({ ...a, r, model: m, fetch: opts.fetchLlm });
119705
+ }
119706
+ const llm = useAi && !opts.client ? await askerFor(config2.llm, process.env, opts.fetchLlm) : void 0;
119503
119707
  const branch = opts.url ? remoteAgentBranch(opts.url) : await localAgentBranch(graph, config2, { llm });
119504
- const model = config2.tests.model;
119505
- const ctx = { client, model, effort: config2.tests.effort, maxTurns: config2.tests.maxTurns, site: config2.site };
119708
+ const ctx = { loop, maxTurns: config2.tests.maxTurns, site: config2.site };
119506
119709
  const enabled = graph.actions.filter((a) => a.enabled);
119507
- out.log(`Testing ${tasks.length} task(s) on ${opts.url ?? "a local copy of your agent branch"}${useClaude ? ` with Claude (${model})` : " in built-in mode (set ANTHROPIC_API_KEY to test with Claude)"}\u2026`);
119710
+ out.log(`Testing ${tasks.length} task(s) on ${opts.url ?? "a local copy of your agent branch"}${useAi ? ` with ${ai.label} (${model})` : ` in built-in mode (set ${apiKeyEnvFor(config2.llm)} to test with an AI agent)`}\u2026`);
119508
119711
  const outcomes = [];
119509
119712
  for (const t of tasks) {
119510
- const agent = useClaude ? await claudeAgentArm(t, { ...ctx, branch, remote: Boolean(opts.url), actions: enabled }) : await builtInAgentArm(t, branch);
119713
+ const agent = useAi ? await aiAgentArm(t, { ...ctx, branch, remote: Boolean(opts.url), actions: enabled }) : await builtInAgentArm(t, branch);
119511
119714
  let human;
119512
119715
  if (t.human && !t.action && t.expect.length) {
119513
- human = useClaude ? await claudeHumanArm(t, { ...ctx, fetch: opts.fetchHuman }) : await crawlForAnswer(config2.site, t.expect, { fetch: opts.fetchHuman });
119716
+ human = useAi ? await aiHumanArm(t, { ...ctx, fetch: opts.fetchHuman }) : await crawlForAnswer(config2.site, t.expect, { fetch: opts.fetchHuman });
119514
119717
  } else if (t.action) human = { ran: false, success: false, requests: 0, tokens: 0, note: "not run: it would submit forms on your live site" };
119515
119718
  outcomes.push({ id: t.id, task: t.task, agent, human });
119516
119719
  out.log(` ${agent.ran ? agent.success ? "\u2713" : "\u2717" : "-"} ${t.id}${agent.note ? `: ${agent.note}` : ""}`);
@@ -119526,8 +119729,8 @@ ${parsed.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n
119526
119729
  const report = {
119527
119730
  site: config2.site,
119528
119731
  target: opts.url ?? "a local copy of the agent branch",
119529
- mode: useClaude ? "claude" : "built-in",
119530
- ...useClaude ? { model } : {},
119732
+ mode: useAi ? "ai" : "built-in",
119733
+ ...useAi ? { provider: ai.label, model } : {},
119531
119734
  ranAt: (/* @__PURE__ */ new Date()).toISOString(),
119532
119735
  score: readinessScore(outcomes, checks),
119533
119736
  tasks: outcomes,
@@ -119568,6 +119771,7 @@ var init_readiness = __esm({
119568
119771
  "packages/cli/src/readiness.ts"() {
119569
119772
  "use strict";
119570
119773
  init_zod();
119774
+ init_src();
119571
119775
  init_src7();
119572
119776
  init_src5();
119573
119777
  init_src3();
@@ -119746,7 +119950,7 @@ ${p.markdown}`).join("\n"));
119746
119950
  const gaps = (await findGaps(project, opts.days)).filter((x) => !known.has(normQ(x.question)) && !siteText.includes(normQ(x.question))).slice(0, 30);
119747
119951
  let llm;
119748
119952
  if (opts.llm) llm = opts.llm;
119749
- else if (opts.llm !== false && config2.llm.enabled && hasClaudeCredentials()) llm = new ClaudeLlm();
119953
+ else if (opts.llm !== false) llm = llmFromSettings(config2.llm)?.llm;
119750
119954
  const added = gaps.length ? await draftSuggestions(g, gaps, { llm, model: config2.llm.askModel }) : [];
119751
119955
  const counts = new Map((await findGaps(project, opts.days)).map((x) => [normQ(x.question), x.asked]));
119752
119956
  const updated = existing.map((s) => ({ ...s, asked: Math.max(s.asked, [s.question, ...s.covers].reduce((n, q) => n + (counts.get(normQ(q)) ?? 0), 0)) }));
@@ -119852,7 +120056,7 @@ async function cmdServe(project, opts, out = { log: (m) => console.log(m) }) {
119852
120056
  const edge = opts.edge ? { ...config2.edge, origin: opts.edge === true ? config2.edge.origin ?? config2.site : opts.edge, publicUrl } : void 0;
119853
120057
  const baseUrl = edge ? `${publicUrl}${config2.edge.prefix}` : config2.deploy.baseUrl ?? publicUrl;
119854
120058
  let llm;
119855
- if (!opts.noLlm && config2.llm.enabled && hasClaudeCredentials()) llm = await createClaudeAsker();
120059
+ if (!opts.noLlm) llm = await askerFor(config2.llm, process.env);
119856
120060
  const hookSecret = opts.hookSecret ?? process.env.AGENTICWEB_HOOK_SECRET;
119857
120061
  const onTraffic = serveHooks.traffic?.(project);
119858
120062
  const engine = await serveHooks.engine?.(project, graph, config2, baseUrl);
@@ -119894,7 +120098,7 @@ async function cmdServe(project, opts, out = { log: (m) => console.log(m) }) {
119894
120098
  out.log(` Ask: ${agentUrl}/ask?q=your+question`);
119895
120099
  out.log(` MCP: ${agentUrl}/mcp`);
119896
120100
  out.log(` Actions: ${engine ? engine.actions().map((a) => a.name).join(", ") || "none enabled" : "none enabled"}`);
119897
- out.log(` /ask uses: ${llm ? `Claude (${config2.llm.askModel})` : "built-in search (set ANTHROPIC_API_KEY for written answers)"}`);
120101
+ out.log(` /ask uses: ${llm ? llm.label : `built-in search (set ${apiKeyEnvFor(config2.llm)} for written answers)`}`);
119898
120102
  if (opts.syncEveryMinutes) out.log(` Sync: every ${opts.syncEveryMinutes} minutes`);
119899
120103
  if (hookSecret) out.log(` Webhook: POST ${agentUrl}/hooks/refresh (header x-agenticweb-secret)`);
119900
120104
  out.log("Press Ctrl+C to stop.");
@@ -120207,8 +120411,13 @@ async function cmdBuild(project, opts = {}, out = stdout) {
120207
120411
  let llm;
120208
120412
  if (opts.llm !== false && config2.llm.enabled) {
120209
120413
  if (opts.llm) llm = opts.llm;
120210
- else if (hasClaudeCredentials()) llm = new ClaudeLlm();
120211
- else out.log(" No Anthropic credentials found: using built-in heuristics. Set ANTHROPIC_API_KEY for richer extraction.");
120414
+ else {
120415
+ const ai = llmFromSettings(config2.llm);
120416
+ if (ai) {
120417
+ llm = ai.llm;
120418
+ out.log(` Reading the site with ${ai.label}.`);
120419
+ } else out.log(` No AI key found: using built-in rules. Set ${apiKeyEnvFor(config2.llm)} for richer extraction.`);
120420
+ }
120212
120421
  }
120213
120422
  const extra = opts.connectors ? await opts.connectors(config2) : await Promise.resolve().then(() => (init_actions_hook(), actions_hook_exports)).then((m) => m.defaultConnectors(config2, (msg) => out.log(` ${msg}`)));
120214
120423
  let graph = await understand(c, {
@@ -120507,7 +120716,7 @@ function requiredEnv(graph, config2, target = "node") {
120507
120716
  for (const v of Object.values(c.headers ?? {})) if (v.startsWith("env:")) env.set(v.slice(4), `Header value for ${a.name}`);
120508
120717
  }
120509
120718
  if (config2.notifications.email === "resend") env.set(config2.notifications.apiKeyEnv, "Resend API key (confirmation emails)");
120510
- env.set("ANTHROPIC_API_KEY", "Optional: written answers on /ask");
120719
+ if (config2.llm.enabled) env.set(config2.llm.apiKeyEnv ?? PROVIDERS[config2.llm.provider].apiKeyEnv, `Optional: ${PROVIDERS[config2.llm.provider].label} key for written answers on /ask`);
120511
120720
  env.set(config2.runtime.apiKeysEnv, 'Optional: agent API keys as "key:agent-name,key2:other"');
120512
120721
  return [...env];
120513
120722
  }
@@ -120659,6 +120868,7 @@ var runtimeTargets;
120659
120868
  var init_deploy_runtime = __esm({
120660
120869
  "packages/cli/src/deploy-runtime.ts"() {
120661
120870
  "use strict";
120871
+ init_src();
120662
120872
  init_project();
120663
120873
  runtimeTargets = ["node", "docker", "cloudflare-worker", "edge"];
120664
120874
  }
@@ -57878,9 +57878,9 @@ var init_client = __esm({
57878
57878
  }
57879
57879
  return url2.toString();
57880
57880
  }
57881
- _calculateNonstreamingTimeout(maxTokens) {
57881
+ _calculateNonstreamingTimeout(maxTokens2) {
57882
57882
  const defaultTimeout = 10 * 60;
57883
- const expectedTimeout = 60 * 60 * maxTokens / 128e3;
57883
+ const expectedTimeout = 60 * 60 * maxTokens2 / 128e3;
57884
57884
  if (expectedTimeout > defaultTimeout) {
57885
57885
  throw new AnthropicError("Streaming is required for operations that may take longer than 10 minutes. See https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/typescript#streaming-responses for more details");
57886
57886
  }
@@ -58230,11 +58230,11 @@ var init_client = __esm({
58230
58230
  const jitter2 = 1 - Math.random() * 0.25;
58231
58231
  return sleepSeconds * jitter2 * 1e3;
58232
58232
  }
58233
- calculateNonstreamingTimeout(maxTokens, maxNonstreamingTokens) {
58233
+ calculateNonstreamingTimeout(maxTokens2, maxNonstreamingTokens) {
58234
58234
  const maxTime = 60 * 60 * 1e3;
58235
58235
  const defaultTime = 60 * 10 * 1e3;
58236
- const expectedTime = maxTime * maxTokens / 128e3;
58237
- if (expectedTime > defaultTime || maxNonstreamingTokens != null && maxTokens > maxNonstreamingTokens) {
58236
+ const expectedTime = maxTime * maxTokens2 / 128e3;
58237
+ if (expectedTime > defaultTime || maxNonstreamingTokens != null && maxTokens2 > maxNonstreamingTokens) {
58238
58238
  throw new AnthropicError("Streaming is required for operations that may take longer than 10 minutes. See https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/typescript#long-requests for more details");
58239
58239
  }
58240
58240
  return defaultTime;
@@ -59392,6 +59392,115 @@ function entityFileName(type) {
59392
59392
  return kebab + "s";
59393
59393
  }
59394
59394
 
59395
+ // packages/core/src/llm-providers.ts
59396
+ var PROVIDER_IDS = ["anthropic", "openai", "deepseek", "gemini", "openai-compatible"];
59397
+ var PROVIDERS = {
59398
+ anthropic: { label: "Claude", apiKeyEnv: "ANTHROPIC_API_KEY", defaultModel: "claude-opus-5-5", jsonMode: "schema", maxTokensParam: "max_tokens" },
59399
+ openai: { label: "OpenAI", baseUrl: "https://api.openai.com/v1", apiKeyEnv: "OPENAI_API_KEY", defaultModel: "gpt-6.1-sol", jsonMode: "schema", maxTokensParam: "max_completion_tokens" },
59400
+ deepseek: { label: "DeepSeek", baseUrl: "https://api.deepseek.com", apiKeyEnv: "DEEPSEEK_API_KEY", defaultModel: "deepseek-flash", jsonMode: "object", maxTokensParam: "max_tokens" },
59401
+ gemini: {
59402
+ label: "Gemini",
59403
+ baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
59404
+ apiKeyEnv: "GEMINI_API_KEY",
59405
+ defaultModel: "gemini-3-flash-preview",
59406
+ jsonMode: "schema",
59407
+ maxTokensParam: "max_tokens"
59408
+ },
59409
+ "openai-compatible": { label: "OpenAI-compatible API", apiKeyEnv: "LLM_API_KEY", defaultModel: "", jsonMode: "object", maxTokensParam: "max_tokens" }
59410
+ };
59411
+ function resolveLlm(s, env) {
59412
+ if (!s.enabled) return void 0;
59413
+ const info = PROVIDERS[s.provider];
59414
+ const keyEnv = s.apiKeyEnv ?? info.apiKeyEnv;
59415
+ const apiKey = env[keyEnv] || void 0;
59416
+ if (s.provider === "anthropic") {
59417
+ if (!apiKey && !env.ANTHROPIC_AUTH_TOKEN && !env.ANTHROPIC_PROFILE) return void 0;
59418
+ } else if (!apiKey && !(s.provider === "openai-compatible" && s.baseUrl)) {
59419
+ return void 0;
59420
+ }
59421
+ const baseUrl = s.baseUrl ?? info.baseUrl;
59422
+ if (s.provider === "openai-compatible" && !baseUrl) throw new Error('llm.provider is "openai-compatible": set llm.baseUrl, e.g. "http://localhost:11434/v1".');
59423
+ return {
59424
+ provider: s.provider,
59425
+ label: info.label,
59426
+ apiKey,
59427
+ baseUrl,
59428
+ jsonMode: info.jsonMode,
59429
+ maxTokensParam: info.maxTokensParam,
59430
+ model: (configured) => {
59431
+ if (s.model) return s.model;
59432
+ if (s.provider === "anthropic") return configured || info.defaultModel;
59433
+ if (configured && !configured.startsWith("claude-")) return configured;
59434
+ if (!info.defaultModel) throw new Error(`Set llm.model to a model your ${info.label} serves.`);
59435
+ return info.defaultModel;
59436
+ }
59437
+ };
59438
+ }
59439
+ async function chatCompletion(r, body, f = fetch) {
59440
+ if (!r.baseUrl) throw new Error(`${r.label} has no API address.`);
59441
+ const res = await f(`${r.baseUrl.replace(/\/+$/, "")}/chat/completions`, {
59442
+ method: "POST",
59443
+ headers: { "content-type": "application/json", ...r.apiKey ? { authorization: `Bearer ${r.apiKey}` } : {} },
59444
+ body: JSON.stringify(body)
59445
+ });
59446
+ if (!res.ok) throw new Error(`${r.label} returned HTTP ${res.status}: ${(await res.text()).slice(0, 300)}`);
59447
+ return await res.json();
59448
+ }
59449
+ var maxTokens = (r, n) => ({ [r.maxTokensParam]: n });
59450
+ function jsonSchemaOf(schema) {
59451
+ const clean = (v) => {
59452
+ if (Array.isArray(v)) return v.map(clean);
59453
+ if (!v || typeof v !== "object") return v;
59454
+ const out = {};
59455
+ for (const [k, val] of Object.entries(v)) {
59456
+ if (k === "$schema") continue;
59457
+ if (k === "type" && Array.isArray(val)) {
59458
+ const types = val.filter((t) => t !== "null");
59459
+ out.type = types[0] ?? "string";
59460
+ if (types.length < val.length) out.nullable = true;
59461
+ continue;
59462
+ }
59463
+ out[k] = clean(val);
59464
+ }
59465
+ return out;
59466
+ };
59467
+ return clean(external_exports.toJSONSchema(schema, { unrepresentable: "any" }));
59468
+ }
59469
+ var parseJson = (text2) => JSON.parse(text2.trim().replace(/^```(?:json)?\s*/i, "").replace(/```\s*$/, ""));
59470
+ async function extractJson(r, args, f = fetch) {
59471
+ const schemaJson = jsonSchemaOf(args.schema);
59472
+ const system = r.jsonMode === "schema" ? args.system : `${args.system}
59473
+
59474
+ Reply with one JSON object only, matching this JSON Schema:
59475
+ ${JSON.stringify(schemaJson)}`;
59476
+ const messages = [
59477
+ { role: "system", content: system },
59478
+ { role: "user", content: args.content }
59479
+ ];
59480
+ const format = r.jsonMode === "schema" ? { type: "json_schema", json_schema: { name: "result", schema: schemaJson, strict: false } } : { type: "json_object" };
59481
+ let problem = "";
59482
+ for (let attempt = 0; attempt < 2; attempt++) {
59483
+ const res = await chatCompletion(r, { model: r.model(args.model), messages, response_format: format, ...maxTokens(r, args.maxTokens ?? 8e3) }, f);
59484
+ const choice = res.choices[0];
59485
+ if (!choice) throw new Error(`${r.label} returned no answer.`);
59486
+ if (choice.message.refusal || choice.finish_reason === "content_filter") throw new Error(`${r.label} declined to process this content.`);
59487
+ const text2 = choice.message.content ?? "";
59488
+ let value;
59489
+ try {
59490
+ value = parseJson(text2);
59491
+ } catch {
59492
+ problem = "The reply wasn't valid JSON.";
59493
+ messages.push({ role: "assistant", content: text2 }, { role: "user", content: `${problem} Reply with the JSON object only.` });
59494
+ continue;
59495
+ }
59496
+ const parsed = args.schema.safeParse(value);
59497
+ if (parsed.success) return parsed.data;
59498
+ problem = parsed.error.issues.slice(0, 5).map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
59499
+ messages.push({ role: "assistant", content: text2 }, { role: "user", content: `That JSON doesn't match the schema (${problem}). Reply with corrected JSON only.` });
59500
+ }
59501
+ throw new Error(`${r.label} didn't return valid data: ${problem}`);
59502
+ }
59503
+
59395
59504
  // packages/core/src/config.ts
59396
59505
  var ActionOverride = external_exports.object({
59397
59506
  name: external_exports.string(),
@@ -59408,6 +59517,7 @@ var ActionOverride = external_exports.object({
59408
59517
  var crawlDefaults = { maxPages: 200, include: [], exclude: [], render: "fetch", respectRobots: true, concurrency: 4 };
59409
59518
  var llmDefaults = {
59410
59519
  enabled: true,
59520
+ provider: "anthropic",
59411
59521
  classifyModel: "claude-opus-5-5",
59412
59522
  extractModel: "claude-opus-5-5",
59413
59523
  actionsModel: "claude-opus-5-5",
@@ -59449,6 +59559,14 @@ var ProjectConfig = external_exports.object({
59449
59559
  actions: external_exports.array(ActionOverride).default([]),
59450
59560
  llm: external_exports.object({
59451
59561
  enabled: external_exports.boolean().default(true),
59562
+ /** Which AI to use: Claude (default), OpenAI, DeepSeek, Gemini, or any OpenAI-compatible API (including local ones). */
59563
+ provider: external_exports.enum(PROVIDER_IDS).default("anthropic"),
59564
+ /** API address for "openai-compatible", e.g. http://localhost:11434/v1 for Ollama. */
59565
+ baseUrl: external_exports.string().url().optional(),
59566
+ /** Env var holding the API key, if not the provider's usual one (ANTHROPIC_API_KEY, OPENAI_API_KEY, DEEPSEEK_API_KEY, GEMINI_API_KEY, LLM_API_KEY). */
59567
+ apiKeyEnv: external_exports.string().optional(),
59568
+ /** One model for every step. Without it, non-Claude providers use their own default model. */
59569
+ model: external_exports.string().optional(),
59452
59570
  classifyModel: external_exports.string().default(llmDefaults.classifyModel),
59453
59571
  extractModel: external_exports.string().default(llmDefaults.extractModel),
59454
59572
  actionsModel: external_exports.string().default(llmDefaults.actionsModel),
@@ -67666,6 +67784,15 @@ async function createClaudeAsker(opts = {}) {
67666
67784
  }
67667
67785
  };
67668
67786
  }
67787
+ async function askerFor(s, env, f) {
67788
+ const r = resolveLlm(s, env);
67789
+ if (!r) return void 0;
67790
+ if (r.provider === "anthropic") {
67791
+ const claude = await createClaudeAsker({ apiKey: r.apiKey });
67792
+ return { label: r.label, extract: (a) => claude.extract({ ...a, model: r.model(a.model) }) };
67793
+ }
67794
+ return { label: r.label, extract: (a) => extractJson(r, a, f) };
67795
+ }
67669
67796
 
67670
67797
  // packages/actions/src/types.ts
67671
67798
  var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
@@ -68628,7 +68755,7 @@ var edge_worker_default = {
68628
68755
  const { AGENTICWEB_KV, ...vars } = env;
68629
68756
  const store = AGENTICWEB_KV ? new KvStore(AGENTICWEB_KV) : new MemoryStore();
68630
68757
  const engine = graph.actions.some((a) => a.enabled) ? createEngine({ graph, config: config2, env: vars, store }) : void 0;
68631
- const llm = config2.llm.enabled && vars.ANTHROPIC_API_KEY ? await createClaudeAsker({ apiKey: vars.ANTHROPIC_API_KEY }) : void 0;
68758
+ const llm = await askerFor(config2.llm, vars);
68632
68759
  edge = createEdge(
68633
68760
  {
68634
68761
  graph,
@@ -57879,9 +57879,9 @@ var init_client = __esm({
57879
57879
  }
57880
57880
  return url2.toString();
57881
57881
  }
57882
- _calculateNonstreamingTimeout(maxTokens) {
57882
+ _calculateNonstreamingTimeout(maxTokens2) {
57883
57883
  const defaultTimeout = 10 * 60;
57884
- const expectedTimeout = 60 * 60 * maxTokens / 128e3;
57884
+ const expectedTimeout = 60 * 60 * maxTokens2 / 128e3;
57885
57885
  if (expectedTimeout > defaultTimeout) {
57886
57886
  throw new AnthropicError("Streaming is required for operations that may take longer than 10 minutes. See https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/typescript#streaming-responses for more details");
57887
57887
  }
@@ -58231,11 +58231,11 @@ var init_client = __esm({
58231
58231
  const jitter2 = 1 - Math.random() * 0.25;
58232
58232
  return sleepSeconds * jitter2 * 1e3;
58233
58233
  }
58234
- calculateNonstreamingTimeout(maxTokens, maxNonstreamingTokens) {
58234
+ calculateNonstreamingTimeout(maxTokens2, maxNonstreamingTokens) {
58235
58235
  const maxTime = 60 * 60 * 1e3;
58236
58236
  const defaultTime = 60 * 10 * 1e3;
58237
- const expectedTime = maxTime * maxTokens / 128e3;
58238
- if (expectedTime > defaultTime || maxNonstreamingTokens != null && maxTokens > maxNonstreamingTokens) {
58237
+ const expectedTime = maxTime * maxTokens2 / 128e3;
58238
+ if (expectedTime > defaultTime || maxNonstreamingTokens != null && maxTokens2 > maxNonstreamingTokens) {
58239
58239
  throw new AnthropicError("Streaming is required for operations that may take longer than 10 minutes. See https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/typescript#long-requests for more details");
58240
58240
  }
58241
58241
  return defaultTime;
@@ -59398,6 +59398,115 @@ function entityFileName(type) {
59398
59398
  return kebab + "s";
59399
59399
  }
59400
59400
 
59401
+ // packages/core/src/llm-providers.ts
59402
+ var PROVIDER_IDS = ["anthropic", "openai", "deepseek", "gemini", "openai-compatible"];
59403
+ var PROVIDERS = {
59404
+ anthropic: { label: "Claude", apiKeyEnv: "ANTHROPIC_API_KEY", defaultModel: "claude-opus-5-5", jsonMode: "schema", maxTokensParam: "max_tokens" },
59405
+ openai: { label: "OpenAI", baseUrl: "https://api.openai.com/v1", apiKeyEnv: "OPENAI_API_KEY", defaultModel: "gpt-6.1-sol", jsonMode: "schema", maxTokensParam: "max_completion_tokens" },
59406
+ deepseek: { label: "DeepSeek", baseUrl: "https://api.deepseek.com", apiKeyEnv: "DEEPSEEK_API_KEY", defaultModel: "deepseek-flash", jsonMode: "object", maxTokensParam: "max_tokens" },
59407
+ gemini: {
59408
+ label: "Gemini",
59409
+ baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
59410
+ apiKeyEnv: "GEMINI_API_KEY",
59411
+ defaultModel: "gemini-3-flash-preview",
59412
+ jsonMode: "schema",
59413
+ maxTokensParam: "max_tokens"
59414
+ },
59415
+ "openai-compatible": { label: "OpenAI-compatible API", apiKeyEnv: "LLM_API_KEY", defaultModel: "", jsonMode: "object", maxTokensParam: "max_tokens" }
59416
+ };
59417
+ function resolveLlm(s, env2) {
59418
+ if (!s.enabled) return void 0;
59419
+ const info = PROVIDERS[s.provider];
59420
+ const keyEnv = s.apiKeyEnv ?? info.apiKeyEnv;
59421
+ const apiKey = env2[keyEnv] || void 0;
59422
+ if (s.provider === "anthropic") {
59423
+ if (!apiKey && !env2.ANTHROPIC_AUTH_TOKEN && !env2.ANTHROPIC_PROFILE) return void 0;
59424
+ } else if (!apiKey && !(s.provider === "openai-compatible" && s.baseUrl)) {
59425
+ return void 0;
59426
+ }
59427
+ const baseUrl = s.baseUrl ?? info.baseUrl;
59428
+ if (s.provider === "openai-compatible" && !baseUrl) throw new Error('llm.provider is "openai-compatible": set llm.baseUrl, e.g. "http://localhost:11434/v1".');
59429
+ return {
59430
+ provider: s.provider,
59431
+ label: info.label,
59432
+ apiKey,
59433
+ baseUrl,
59434
+ jsonMode: info.jsonMode,
59435
+ maxTokensParam: info.maxTokensParam,
59436
+ model: (configured) => {
59437
+ if (s.model) return s.model;
59438
+ if (s.provider === "anthropic") return configured || info.defaultModel;
59439
+ if (configured && !configured.startsWith("claude-")) return configured;
59440
+ if (!info.defaultModel) throw new Error(`Set llm.model to a model your ${info.label} serves.`);
59441
+ return info.defaultModel;
59442
+ }
59443
+ };
59444
+ }
59445
+ async function chatCompletion(r, body, f = fetch) {
59446
+ if (!r.baseUrl) throw new Error(`${r.label} has no API address.`);
59447
+ const res = await f(`${r.baseUrl.replace(/\/+$/, "")}/chat/completions`, {
59448
+ method: "POST",
59449
+ headers: { "content-type": "application/json", ...r.apiKey ? { authorization: `Bearer ${r.apiKey}` } : {} },
59450
+ body: JSON.stringify(body)
59451
+ });
59452
+ if (!res.ok) throw new Error(`${r.label} returned HTTP ${res.status}: ${(await res.text()).slice(0, 300)}`);
59453
+ return await res.json();
59454
+ }
59455
+ var maxTokens = (r, n) => ({ [r.maxTokensParam]: n });
59456
+ function jsonSchemaOf(schema) {
59457
+ const clean = (v) => {
59458
+ if (Array.isArray(v)) return v.map(clean);
59459
+ if (!v || typeof v !== "object") return v;
59460
+ const out = {};
59461
+ for (const [k, val] of Object.entries(v)) {
59462
+ if (k === "$schema") continue;
59463
+ if (k === "type" && Array.isArray(val)) {
59464
+ const types = val.filter((t) => t !== "null");
59465
+ out.type = types[0] ?? "string";
59466
+ if (types.length < val.length) out.nullable = true;
59467
+ continue;
59468
+ }
59469
+ out[k] = clean(val);
59470
+ }
59471
+ return out;
59472
+ };
59473
+ return clean(external_exports.toJSONSchema(schema, { unrepresentable: "any" }));
59474
+ }
59475
+ var parseJson = (text2) => JSON.parse(text2.trim().replace(/^```(?:json)?\s*/i, "").replace(/```\s*$/, ""));
59476
+ async function extractJson(r, args, f = fetch) {
59477
+ const schemaJson = jsonSchemaOf(args.schema);
59478
+ const system = r.jsonMode === "schema" ? args.system : `${args.system}
59479
+
59480
+ Reply with one JSON object only, matching this JSON Schema:
59481
+ ${JSON.stringify(schemaJson)}`;
59482
+ const messages = [
59483
+ { role: "system", content: system },
59484
+ { role: "user", content: args.content }
59485
+ ];
59486
+ const format = r.jsonMode === "schema" ? { type: "json_schema", json_schema: { name: "result", schema: schemaJson, strict: false } } : { type: "json_object" };
59487
+ let problem = "";
59488
+ for (let attempt = 0; attempt < 2; attempt++) {
59489
+ const res = await chatCompletion(r, { model: r.model(args.model), messages, response_format: format, ...maxTokens(r, args.maxTokens ?? 8e3) }, f);
59490
+ const choice = res.choices[0];
59491
+ if (!choice) throw new Error(`${r.label} returned no answer.`);
59492
+ if (choice.message.refusal || choice.finish_reason === "content_filter") throw new Error(`${r.label} declined to process this content.`);
59493
+ const text2 = choice.message.content ?? "";
59494
+ let value;
59495
+ try {
59496
+ value = parseJson(text2);
59497
+ } catch {
59498
+ problem = "The reply wasn't valid JSON.";
59499
+ messages.push({ role: "assistant", content: text2 }, { role: "user", content: `${problem} Reply with the JSON object only.` });
59500
+ continue;
59501
+ }
59502
+ const parsed = args.schema.safeParse(value);
59503
+ if (parsed.success) return parsed.data;
59504
+ problem = parsed.error.issues.slice(0, 5).map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
59505
+ messages.push({ role: "assistant", content: text2 }, { role: "user", content: `That JSON doesn't match the schema (${problem}). Reply with corrected JSON only.` });
59506
+ }
59507
+ throw new Error(`${r.label} didn't return valid data: ${problem}`);
59508
+ }
59509
+
59401
59510
  // packages/core/src/config.ts
59402
59511
  var ActionOverride = external_exports.object({
59403
59512
  name: external_exports.string(),
@@ -59414,6 +59523,7 @@ var ActionOverride = external_exports.object({
59414
59523
  var crawlDefaults = { maxPages: 200, include: [], exclude: [], render: "fetch", respectRobots: true, concurrency: 4 };
59415
59524
  var llmDefaults = {
59416
59525
  enabled: true,
59526
+ provider: "anthropic",
59417
59527
  classifyModel: "claude-opus-5-5",
59418
59528
  extractModel: "claude-opus-5-5",
59419
59529
  actionsModel: "claude-opus-5-5",
@@ -59455,6 +59565,14 @@ var ProjectConfig = external_exports.object({
59455
59565
  actions: external_exports.array(ActionOverride).default([]),
59456
59566
  llm: external_exports.object({
59457
59567
  enabled: external_exports.boolean().default(true),
59568
+ /** Which AI to use: Claude (default), OpenAI, DeepSeek, Gemini, or any OpenAI-compatible API (including local ones). */
59569
+ provider: external_exports.enum(PROVIDER_IDS).default("anthropic"),
59570
+ /** API address for "openai-compatible", e.g. http://localhost:11434/v1 for Ollama. */
59571
+ baseUrl: external_exports.string().url().optional(),
59572
+ /** Env var holding the API key, if not the provider's usual one (ANTHROPIC_API_KEY, OPENAI_API_KEY, DEEPSEEK_API_KEY, GEMINI_API_KEY, LLM_API_KEY). */
59573
+ apiKeyEnv: external_exports.string().optional(),
59574
+ /** One model for every step. Without it, non-Claude providers use their own default model. */
59575
+ model: external_exports.string().optional(),
59458
59576
  classifyModel: external_exports.string().default(llmDefaults.classifyModel),
59459
59577
  extractModel: external_exports.string().default(llmDefaults.extractModel),
59460
59578
  actionsModel: external_exports.string().default(llmDefaults.actionsModel),
@@ -67672,6 +67790,15 @@ async function createClaudeAsker(opts = {}) {
67672
67790
  }
67673
67791
  };
67674
67792
  }
67793
+ async function askerFor(s, env2, f) {
67794
+ const r = resolveLlm(s, env2);
67795
+ if (!r) return void 0;
67796
+ if (r.provider === "anthropic") {
67797
+ const claude = await createClaudeAsker({ apiKey: r.apiKey });
67798
+ return { label: r.label, extract: (a) => claude.extract({ ...a, model: r.model(a.model) }) };
67799
+ }
67800
+ return { label: r.label, extract: (a) => extractJson(r, a, f) };
67801
+ }
67675
67802
 
67676
67803
  // node_modules/@hono/node-server/dist/constants-BLSFu_RU.mjs
67677
67804
  var X_ALREADY_SENT = "x-hono-already-sent";
@@ -69991,7 +70118,7 @@ var env = process.env;
69991
70118
  var graph = SiteGraph.parse(JSON.parse(readFileSync2(path5.join(here, "site-graph.json"), "utf8")));
69992
70119
  var config2 = ProjectConfig.parse(JSON.parse(readFileSync2(path5.join(here, "config.json"), "utf8")));
69993
70120
  var engine = graph.actions.some((a) => a.enabled) ? createEngine({ graph, config: config2, env, store: new FileStore2(path5.join(env.DATA_DIR ?? here, "actions.json")) }) : void 0;
69994
- var llm = config2.llm.enabled && (env.ANTHROPIC_API_KEY || env.ANTHROPIC_AUTH_TOKEN) ? await createClaudeAsker() : void 0;
70121
+ var llm = await askerFor(config2.llm, env);
69995
70122
  var edgeOrigin = env.EDGE_ORIGIN || (env.EDGE_MODE === "1" ? config2.edge.origin : void 0);
69996
70123
  var server = await serveNode({
69997
70124
  graph,
@@ -57878,9 +57878,9 @@ var init_client = __esm({
57878
57878
  }
57879
57879
  return url2.toString();
57880
57880
  }
57881
- _calculateNonstreamingTimeout(maxTokens) {
57881
+ _calculateNonstreamingTimeout(maxTokens2) {
57882
57882
  const defaultTimeout = 10 * 60;
57883
- const expectedTimeout = 60 * 60 * maxTokens / 128e3;
57883
+ const expectedTimeout = 60 * 60 * maxTokens2 / 128e3;
57884
57884
  if (expectedTimeout > defaultTimeout) {
57885
57885
  throw new AnthropicError("Streaming is required for operations that may take longer than 10 minutes. See https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/typescript#streaming-responses for more details");
57886
57886
  }
@@ -58230,11 +58230,11 @@ var init_client = __esm({
58230
58230
  const jitter2 = 1 - Math.random() * 0.25;
58231
58231
  return sleepSeconds * jitter2 * 1e3;
58232
58232
  }
58233
- calculateNonstreamingTimeout(maxTokens, maxNonstreamingTokens) {
58233
+ calculateNonstreamingTimeout(maxTokens2, maxNonstreamingTokens) {
58234
58234
  const maxTime = 60 * 60 * 1e3;
58235
58235
  const defaultTime = 60 * 10 * 1e3;
58236
- const expectedTime = maxTime * maxTokens / 128e3;
58237
- if (expectedTime > defaultTime || maxNonstreamingTokens != null && maxTokens > maxNonstreamingTokens) {
58236
+ const expectedTime = maxTime * maxTokens2 / 128e3;
58237
+ if (expectedTime > defaultTime || maxNonstreamingTokens != null && maxTokens2 > maxNonstreamingTokens) {
58238
58238
  throw new AnthropicError("Streaming is required for operations that may take longer than 10 minutes. See https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/typescript#long-requests for more details");
58239
58239
  }
58240
58240
  return defaultTime;
@@ -59392,6 +59392,115 @@ function entityFileName(type) {
59392
59392
  return kebab + "s";
59393
59393
  }
59394
59394
 
59395
+ // packages/core/src/llm-providers.ts
59396
+ var PROVIDER_IDS = ["anthropic", "openai", "deepseek", "gemini", "openai-compatible"];
59397
+ var PROVIDERS = {
59398
+ anthropic: { label: "Claude", apiKeyEnv: "ANTHROPIC_API_KEY", defaultModel: "claude-opus-5-5", jsonMode: "schema", maxTokensParam: "max_tokens" },
59399
+ openai: { label: "OpenAI", baseUrl: "https://api.openai.com/v1", apiKeyEnv: "OPENAI_API_KEY", defaultModel: "gpt-6.1-sol", jsonMode: "schema", maxTokensParam: "max_completion_tokens" },
59400
+ deepseek: { label: "DeepSeek", baseUrl: "https://api.deepseek.com", apiKeyEnv: "DEEPSEEK_API_KEY", defaultModel: "deepseek-flash", jsonMode: "object", maxTokensParam: "max_tokens" },
59401
+ gemini: {
59402
+ label: "Gemini",
59403
+ baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
59404
+ apiKeyEnv: "GEMINI_API_KEY",
59405
+ defaultModel: "gemini-3-flash-preview",
59406
+ jsonMode: "schema",
59407
+ maxTokensParam: "max_tokens"
59408
+ },
59409
+ "openai-compatible": { label: "OpenAI-compatible API", apiKeyEnv: "LLM_API_KEY", defaultModel: "", jsonMode: "object", maxTokensParam: "max_tokens" }
59410
+ };
59411
+ function resolveLlm(s, env) {
59412
+ if (!s.enabled) return void 0;
59413
+ const info = PROVIDERS[s.provider];
59414
+ const keyEnv = s.apiKeyEnv ?? info.apiKeyEnv;
59415
+ const apiKey = env[keyEnv] || void 0;
59416
+ if (s.provider === "anthropic") {
59417
+ if (!apiKey && !env.ANTHROPIC_AUTH_TOKEN && !env.ANTHROPIC_PROFILE) return void 0;
59418
+ } else if (!apiKey && !(s.provider === "openai-compatible" && s.baseUrl)) {
59419
+ return void 0;
59420
+ }
59421
+ const baseUrl = s.baseUrl ?? info.baseUrl;
59422
+ if (s.provider === "openai-compatible" && !baseUrl) throw new Error('llm.provider is "openai-compatible": set llm.baseUrl, e.g. "http://localhost:11434/v1".');
59423
+ return {
59424
+ provider: s.provider,
59425
+ label: info.label,
59426
+ apiKey,
59427
+ baseUrl,
59428
+ jsonMode: info.jsonMode,
59429
+ maxTokensParam: info.maxTokensParam,
59430
+ model: (configured) => {
59431
+ if (s.model) return s.model;
59432
+ if (s.provider === "anthropic") return configured || info.defaultModel;
59433
+ if (configured && !configured.startsWith("claude-")) return configured;
59434
+ if (!info.defaultModel) throw new Error(`Set llm.model to a model your ${info.label} serves.`);
59435
+ return info.defaultModel;
59436
+ }
59437
+ };
59438
+ }
59439
+ async function chatCompletion(r, body, f = fetch) {
59440
+ if (!r.baseUrl) throw new Error(`${r.label} has no API address.`);
59441
+ const res = await f(`${r.baseUrl.replace(/\/+$/, "")}/chat/completions`, {
59442
+ method: "POST",
59443
+ headers: { "content-type": "application/json", ...r.apiKey ? { authorization: `Bearer ${r.apiKey}` } : {} },
59444
+ body: JSON.stringify(body)
59445
+ });
59446
+ if (!res.ok) throw new Error(`${r.label} returned HTTP ${res.status}: ${(await res.text()).slice(0, 300)}`);
59447
+ return await res.json();
59448
+ }
59449
+ var maxTokens = (r, n) => ({ [r.maxTokensParam]: n });
59450
+ function jsonSchemaOf(schema) {
59451
+ const clean = (v) => {
59452
+ if (Array.isArray(v)) return v.map(clean);
59453
+ if (!v || typeof v !== "object") return v;
59454
+ const out = {};
59455
+ for (const [k, val] of Object.entries(v)) {
59456
+ if (k === "$schema") continue;
59457
+ if (k === "type" && Array.isArray(val)) {
59458
+ const types = val.filter((t) => t !== "null");
59459
+ out.type = types[0] ?? "string";
59460
+ if (types.length < val.length) out.nullable = true;
59461
+ continue;
59462
+ }
59463
+ out[k] = clean(val);
59464
+ }
59465
+ return out;
59466
+ };
59467
+ return clean(external_exports.toJSONSchema(schema, { unrepresentable: "any" }));
59468
+ }
59469
+ var parseJson = (text2) => JSON.parse(text2.trim().replace(/^```(?:json)?\s*/i, "").replace(/```\s*$/, ""));
59470
+ async function extractJson(r, args, f = fetch) {
59471
+ const schemaJson = jsonSchemaOf(args.schema);
59472
+ const system = r.jsonMode === "schema" ? args.system : `${args.system}
59473
+
59474
+ Reply with one JSON object only, matching this JSON Schema:
59475
+ ${JSON.stringify(schemaJson)}`;
59476
+ const messages = [
59477
+ { role: "system", content: system },
59478
+ { role: "user", content: args.content }
59479
+ ];
59480
+ const format = r.jsonMode === "schema" ? { type: "json_schema", json_schema: { name: "result", schema: schemaJson, strict: false } } : { type: "json_object" };
59481
+ let problem = "";
59482
+ for (let attempt = 0; attempt < 2; attempt++) {
59483
+ const res = await chatCompletion(r, { model: r.model(args.model), messages, response_format: format, ...maxTokens(r, args.maxTokens ?? 8e3) }, f);
59484
+ const choice = res.choices[0];
59485
+ if (!choice) throw new Error(`${r.label} returned no answer.`);
59486
+ if (choice.message.refusal || choice.finish_reason === "content_filter") throw new Error(`${r.label} declined to process this content.`);
59487
+ const text2 = choice.message.content ?? "";
59488
+ let value;
59489
+ try {
59490
+ value = parseJson(text2);
59491
+ } catch {
59492
+ problem = "The reply wasn't valid JSON.";
59493
+ messages.push({ role: "assistant", content: text2 }, { role: "user", content: `${problem} Reply with the JSON object only.` });
59494
+ continue;
59495
+ }
59496
+ const parsed = args.schema.safeParse(value);
59497
+ if (parsed.success) return parsed.data;
59498
+ problem = parsed.error.issues.slice(0, 5).map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
59499
+ messages.push({ role: "assistant", content: text2 }, { role: "user", content: `That JSON doesn't match the schema (${problem}). Reply with corrected JSON only.` });
59500
+ }
59501
+ throw new Error(`${r.label} didn't return valid data: ${problem}`);
59502
+ }
59503
+
59395
59504
  // packages/core/src/config.ts
59396
59505
  var ActionOverride = external_exports.object({
59397
59506
  name: external_exports.string(),
@@ -59408,6 +59517,7 @@ var ActionOverride = external_exports.object({
59408
59517
  var crawlDefaults = { maxPages: 200, include: [], exclude: [], render: "fetch", respectRobots: true, concurrency: 4 };
59409
59518
  var llmDefaults = {
59410
59519
  enabled: true,
59520
+ provider: "anthropic",
59411
59521
  classifyModel: "claude-opus-5-5",
59412
59522
  extractModel: "claude-opus-5-5",
59413
59523
  actionsModel: "claude-opus-5-5",
@@ -59449,6 +59559,14 @@ var ProjectConfig = external_exports.object({
59449
59559
  actions: external_exports.array(ActionOverride).default([]),
59450
59560
  llm: external_exports.object({
59451
59561
  enabled: external_exports.boolean().default(true),
59562
+ /** Which AI to use: Claude (default), OpenAI, DeepSeek, Gemini, or any OpenAI-compatible API (including local ones). */
59563
+ provider: external_exports.enum(PROVIDER_IDS).default("anthropic"),
59564
+ /** API address for "openai-compatible", e.g. http://localhost:11434/v1 for Ollama. */
59565
+ baseUrl: external_exports.string().url().optional(),
59566
+ /** Env var holding the API key, if not the provider's usual one (ANTHROPIC_API_KEY, OPENAI_API_KEY, DEEPSEEK_API_KEY, GEMINI_API_KEY, LLM_API_KEY). */
59567
+ apiKeyEnv: external_exports.string().optional(),
59568
+ /** One model for every step. Without it, non-Claude providers use their own default model. */
59569
+ model: external_exports.string().optional(),
59452
59570
  classifyModel: external_exports.string().default(llmDefaults.classifyModel),
59453
59571
  extractModel: external_exports.string().default(llmDefaults.extractModel),
59454
59572
  actionsModel: external_exports.string().default(llmDefaults.actionsModel),
@@ -67461,6 +67579,15 @@ async function createClaudeAsker(opts = {}) {
67461
67579
  }
67462
67580
  };
67463
67581
  }
67582
+ async function askerFor(s, env, f) {
67583
+ const r = resolveLlm(s, env);
67584
+ if (!r) return void 0;
67585
+ if (r.provider === "anthropic") {
67586
+ const claude = await createClaudeAsker({ apiKey: r.apiKey });
67587
+ return { label: r.label, extract: (a) => claude.extract({ ...a, model: r.model(a.model) }) };
67588
+ }
67589
+ return { label: r.label, extract: (a) => extractJson(r, a, f) };
67590
+ }
67464
67591
 
67465
67592
  // packages/actions/src/types.ts
67466
67593
  var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
@@ -68423,7 +68550,7 @@ var worker_default = {
68423
68550
  const { AGENTICWEB_KV, ...vars } = env;
68424
68551
  const store = AGENTICWEB_KV ? new KvStore(AGENTICWEB_KV) : new MemoryStore();
68425
68552
  const engine = graph.actions.some((a) => a.enabled) ? createEngine({ graph, config: config2, env: vars, store }) : void 0;
68426
- const llm = config2.llm.enabled && vars.ANTHROPIC_API_KEY ? await createClaudeAsker({ apiKey: vars.ANTHROPIC_API_KEY }) : void 0;
68553
+ const llm = await askerFor(config2.llm, vars);
68427
68554
  app = createApp({
68428
68555
  graph,
68429
68556
  engine,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agenticweb-cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Turn a website you own into an agent-ready site: llms.txt, schema.org data, an MCP server and safe actions.",
5
5
  "keywords": [
6
6
  "ai-agents",