Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/api/routes/mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import { Hono } from "hono";
import { dispatchToolCall } from "../../mcp/tool-dispatcher.js";
import { resolveAiModel, extractAiText } from "../../lib/ai-model.js";
import { MCP_TOOLS } from "../../mcp/tool-registry.js";

const mcpRoutes = new Hono();
Expand Down Expand Up @@ -463,8 +464,7 @@ mcpRoutes.post("/sampling/sample", async (c) => {
aiMessages.push({ role: msg.role, content: textContent });
}

const model =
modelPreferences?.hints?.[0]?.name || "@cf/meta/llama-3.1-8b-instruct";
const model = modelPreferences?.hints?.[0]?.name || resolveAiModel(c.env);

const aiResult = await c.env.AI.run(model, {
messages: aiMessages,
Expand All @@ -474,7 +474,7 @@ mcpRoutes.post("/sampling/sample", async (c) => {
return c.json({
model,
role: "assistant",
content: { type: "text", text: aiResult.response },
content: { type: "text", text: extractAiText(aiResult) },
stopReason: "endTurn",
});
} catch (error) {
Expand Down
5 changes: 3 additions & 2 deletions src/api/routes/prompts.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/

import { Hono } from "hono";
import { resolveAiModel, extractAiText } from "../../lib/ai-model.js";

export const promptRoutes = new Hono();

Expand Down Expand Up @@ -356,7 +357,7 @@ promptRoutes.post("/execute", async (c) => {
// Direct AI proxy via Workers AI
if (c.env.AI) {
const aiResult = await c.env.AI.run(
c.env.AI_MODEL_PRIMARY || "@cf/meta/llama-4-scout-17b-16e-instruct",
resolveAiModel(c.env),
{
messages: [
{ role: "system", content: composedPrompt },
Expand All @@ -365,7 +366,7 @@ promptRoutes.post("/execute", async (c) => {
max_tokens: body.maxTokens || 4096,
}
);
result = aiResult.response;
result = extractAiText(aiResult);
executedBy = "chittyconnect/workers-ai";
} else {
throw new Error("No AI binding or dispatch target available");
Expand Down
9 changes: 5 additions & 4 deletions src/intelligence/cognitive-coordination.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import { ContextConsciousness } from "./context-consciousness.js";
import { resolveAiModel, extractAiText } from "../lib/ai-model.js";
import { MemoryCloude } from "./memory-cloude.js";

/**
Expand Down Expand Up @@ -366,7 +367,7 @@ Respond in JSON format: {
"risks": ["..."]
}`;

const response = await this.env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
const response = await this.env.AI.run(resolveAiModel(this.env), {
messages: [
{
role: "system",
Expand All @@ -380,7 +381,7 @@ Respond in JSON format: {
],
});

const analysis = JSON.parse(response.response);
const analysis = JSON.parse(extractAiText(response));
console.log(
`[Cognitive-Coordination™] Task complexity: ${analysis.complexity}`,
);
Expand Down Expand Up @@ -492,7 +493,7 @@ Respond in JSON format: {
"recommendations": ["..."]
}`;

const response = await this.env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
const response = await this.env.AI.run(resolveAiModel(this.env), {
messages: [
{
role: "system",
Expand All @@ -506,7 +507,7 @@ Respond in JSON format: {
],
});

const synthesis = JSON.parse(response.response);
const synthesis = JSON.parse(extractAiText(response));

return {
success: true,
Expand Down
6 changes: 4 additions & 2 deletions src/intelligence/context-consciousness.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
* @module intelligence/context-consciousness
*/

import { resolveAiModel, extractAiText } from "../lib/ai-model.js";

export class ContextConsciousness {
constructor(env) {
this.env = env;
Expand Down Expand Up @@ -197,7 +199,7 @@ Look for:

Respond in JSON format: {"anomalies": [{"type": "...", "description": "...", "severity": "low|medium|high"}]}`;

const response = await this.env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
const response = await this.env.AI.run(resolveAiModel(this.env), {
messages: [
{
role: "system",
Expand All @@ -208,7 +210,7 @@ Respond in JSON format: {"anomalies": [{"type": "...", "description": "...", "se
],
});

const result = JSON.parse(response.response);
const result = JSON.parse(extractAiText(response));
return result.anomalies || [];
} catch (error) {
console.warn(
Expand Down
6 changes: 4 additions & 2 deletions src/intelligence/intent-predictor.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
* @module intelligence/intent-predictor
*/

import { resolveAiModel, extractAiText } from "../lib/ai-model.js";

export class IntentPredictor {
constructor(env, deps = {}) {
this.env = env;
Expand Down Expand Up @@ -582,7 +584,7 @@ Prediction: ${JSON.stringify(prediction)}

Return strict JSON with keys:
intent, candidateIntents, suggestedServices, preloadData, nextActions, confidence, signals, historySummary`;
const response = await this.ai.run("@cf/meta/llama-3.1-8b-instruct", {
const response = await this.ai.run(resolveAiModel(this.env), {
messages: [
{
role: "system",
Expand All @@ -596,7 +598,7 @@ intent, candidateIntents, suggestedServices, preloadData, nextActions, confidenc
],
});

const text = String(response?.response || "").trim();
const text = extractAiText(response).trim();
const json = this.extractJson(text);
if (!json) return null;

Expand Down
11 changes: 9 additions & 2 deletions src/intelligence/memory-cloude.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
* @canon chittycanon://gov/governance#core-types
*/

import { resolveAiModel, extractAiText } from "../lib/ai-model.js";

export class MemoryCloude {
constructor(env) {
this.env = env;
Expand Down Expand Up @@ -360,7 +362,7 @@ export class MemoryCloude {

try {
// Use AI to generate summary
const response = await this.env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
const response = await this.env.AI.run(resolveAiModel(this.env), {
messages: [
{
role: "system",
Expand All @@ -374,7 +376,12 @@ export class MemoryCloude {
],
});

const summary = response.response;
const summary = extractAiText(response);
if (!summary) {
// An empty envelope must not be cached as a valid summary — fall through
// to the catch below so the failure stays visible instead of persisting "".
throw new Error("Workers AI returned no summary text");
}
Comment thread
chitcommit marked this conversation as resolved.

// Store summary
await this.kv.put(`session:${sessionId}:summary`, summary, {
Expand Down
6 changes: 4 additions & 2 deletions src/intelligence/relationship-engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
* @module intelligence/relationship-engine
*/

import { resolveAiModel, extractAiText } from "../lib/ai-model.js";

export class RelationshipEngine {
constructor(env) {
this.env = env;
Expand Down Expand Up @@ -448,7 +450,7 @@ export class RelationshipEngine {
if (!this.ai) return null;

try {
const response = await this.ai.run("@cf/meta/llama-3.1-8b-instruct", {
const response = await this.ai.run(resolveAiModel(this.env), {
messages: [
{
role: "system",
Expand All @@ -469,7 +471,7 @@ export class RelationshipEngine {
],
});

return response?.response || null;
return extractAiText(response) || null;
} catch (error) {
console.warn(
"[RelationshipEngine] Summary generation failed:",
Expand Down
38 changes: 38 additions & 0 deletions src/lib/ai-model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Workers AI model resolution for the ChittyConnect intelligence layer.
*
* `@cf/meta/llama-3.1-8b-instruct` aliases `@cf/meta/infire-llama-3.1-8b-instruct`,
* which Cloudflare deprecated on 2026-05-30. Calls to it fail with error 5028, so
* every intelligence-layer AI path that hardcoded it has been silently degraded
* since that date. Resolve the model through this module instead of inlining an id,
* so the next deprecation is a single env var away from being handled.
*/

export const AI_MODEL_DEFAULT = "@cf/meta/llama-4-scout-17b-16e-instruct";

/**
* @param {{AI_MODEL_PRIMARY?: string}} env
* @returns {string} model id to pass to env.AI.run()
*/
export function resolveAiModel(env) {
return env?.AI_MODEL_PRIMARY || AI_MODEL_DEFAULT;
}

/**
* Workers AI returns a chat-completions envelope that carries the generated text
* both as a top-level `response` string and under `choices[0].message.content`.
* Read both so a model whose envelope omits either shape does not degrade to
* `undefined` — an undefined summary is a silent failure, which is worse than the
* loud one it would replace.
*
* @param {unknown} result value returned by env.AI.run()
* @returns {string} generated text, or "" when the envelope carries none
*/
export function extractAiText(result) {
if (typeof result === "string") return result;
if (!result || typeof result !== "object") return "";
if (typeof result.response === "string") return result.response;
const choice = Array.isArray(result.choices) ? result.choices[0] : null;
const content = choice?.message?.content;
return typeof content === "string" ? content : "";
Comment thread
chitcommit marked this conversation as resolved.
Outdated
}
89 changes: 89 additions & 0 deletions tests/lib/ai-model.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* Tests for Workers AI model resolution.
*
* The envelopes below are not hand-written fixtures — they are the verbatim
* payloads returned by production Workers AI on 2026-09-04 via
* POST https://connect.chitty.cc/api/thirdparty/cloudflare/ai/run, recorded so
* the parser is exercised against the shape the platform actually emits rather
* than the shape we assume it emits.
*/

import { describe, it, expect } from "vitest";
import {
AI_MODEL_DEFAULT,
resolveAiModel,
extractAiText,
} from "../../src/lib/ai-model.js";

// Recorded live: model "@cf/meta/llama-4-scout-17b-16e-instruct", prompt "reply with the word ok"
const LIVE_SCOUT_ENVELOPE = {
choices: [
{
finish_reason: "stop",
index: 0,
logprobs: null,
message: {
annotations: null,
audio: null,
content: "ok",
function_call: null,
reasoning: null,
refusal: null,
role: "assistant",
},
routed_experts: null,
stop_reason: null,
token_ids: null,
},
],
created: 1788561543,
id: "chatcmpl-a318096f-ca2c-423a-a8bc-4813ca68c36f",
model: "@cf/meta/llama-4-scout-17b-16e-instruct",
object: "chat.completion",
response: "ok",
tool_calls: [],
usage: { prompt_tokens: 15, completion_tokens: 2, total_tokens: 17 },
};

describe("resolveAiModel", () => {
it("defaults to a model that is live in production, not the 2026-05-30 deprecated one", () => {
expect(resolveAiModel({})).toBe(AI_MODEL_DEFAULT);
expect(AI_MODEL_DEFAULT).not.toContain("llama-3.1-8b-instruct");
});

it("honours the AI_MODEL_PRIMARY override so the next deprecation needs no code change", () => {
expect(
resolveAiModel({
AI_MODEL_PRIMARY: "@cf/meta/llama-3.3-70b-instruct-fp8-fast",
}),
).toBe("@cf/meta/llama-3.3-70b-instruct-fp8-fast");
});

it("tolerates a missing env rather than throwing inside a catch-wrapped AI path", () => {
expect(resolveAiModel(undefined)).toBe(AI_MODEL_DEFAULT);
});
});

describe("extractAiText", () => {
it("reads the live production envelope", () => {
expect(extractAiText(LIVE_SCOUT_ENVELOPE)).toBe("ok");
});

it("still reads the envelope when the compat `response` field is absent", () => {
const { response: _dropped, ...choicesOnly } = LIVE_SCOUT_ENVELOPE;
expect(extractAiText(choicesOnly)).toBe("ok");
});

it("returns empty string — never undefined — for an envelope carrying no text", () => {
// A caller that stores undefined turns a loud failure into a silent one.
for (const empty of [
{},
{ choices: [] },
{ choices: [{ message: {} }] },
null,
7,
]) {
expect(extractAiText(empty)).toBe("");
}
});
});
Loading