Skip to content
Merged
18 changes: 18 additions & 0 deletions packages/ai/scripts/generate-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2269,6 +2269,24 @@ async function generateModels() {
!((model.provider === "opencode" || model.provider === "opencode-go") && model.id === "gpt-5.3-codex-spark"),
);

// Cloudflare AI Gateway exposes Workers AI models through its OpenAI-compatible
// endpoint even when models.dev omits the duplicated gateway catalog entries.
for (const model of allModels.filter((candidate) => candidate.provider === "cloudflare-workers-ai")) {
const id = `workers-ai/${model.id}`;
if (allModels.some((candidate) => candidate.provider === "cloudflare-ai-gateway" && candidate.id === id)) continue;
const gatewayModel = {
...model,
id,
provider: "cloudflare-ai-gateway",
baseUrl: CLOUDFLARE_AI_GATEWAY_COMPAT_BASE_URL,
cost: { ...model.cost },
compat: { ...model.compat, sendSessionAffinityHeaders: true },
};
allModels.push(gatewayModel);
const reasoningOptions = modelsDevReasoningOptions.get(getModelKey(model));
if (reasoningOptions) modelsDevReasoningOptions.set(getModelKey(gatewayModel), reasoningOptions);
}

// Temporary overrides until upstream model metadata is corrected.
for (const candidate of allModels) {
if (candidate.provider === "github-copilot" && GITHUB_COPILOT_EXTENDED_CONTEXT_MODELS.has(candidate.id)) {
Expand Down
8 changes: 4 additions & 4 deletions packages/ai/src/providers/cloudflare-ai-gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { CLOUDFLARE_AI_GATEWAY_MODELS } from "./cloudflare-ai-gateway.models.ts"
import { cloudflareAIGatewayAuth } from "./cloudflare-auth.ts";
import { cloudflareStreams } from "./cloudflare-stream.ts";

export function cloudflareAIGatewayProvider(): Provider<
"anthropic-messages" | "openai-completions" | "openai-responses"
> {
return createProvider({
type CloudflareAIGatewayApi = "anthropic-messages" | "openai-completions" | "openai-responses";

export function cloudflareAIGatewayProvider(): Provider<CloudflareAIGatewayApi> {
return createProvider<CloudflareAIGatewayApi>({
id: "cloudflare-ai-gateway",
name: "Cloudflare AI Gateway",
auth: { apiKey: cloudflareAIGatewayAuth() },
Expand Down
66 changes: 59 additions & 7 deletions packages/coding-agent/src/core/evolution-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { type AssistantMessage, contentText, type Message, type Model, type Tool
import type { HarnessStateStore, HarnessVersion } from "@earendil-works/diffex-harness-state";
import lockfile from "proper-lockfile";
import type { TSchema } from "typebox";
import { Check, Errors } from "typebox/value";
import {
classifyEvolutionEvidence,
computeEvolutionContentDigest,
Expand All @@ -15,6 +16,7 @@ import {
EvolutionControlPlane,
EvolutionControlService,
type EvolutionEvidenceCatalog,
EvolutionExecutionError,
type EvolutionHarnessVersionInfo,
type EvolutionJobDetail,
type EvolutionJobRunner,
Expand Down Expand Up @@ -127,6 +129,25 @@ function evolutionToolError(error: unknown): JsonObject {
};
}

function validateEvolutionOutput(request: EvolutionModelRequest, output: JsonObject): string | null {
const schema = request.outputSchema as TSchema;
if (!Check(schema, output)) {
const issue = [...Errors(schema, output)][0];
return issue?.message ?? "Response does not match the required schema";
}
if (request.stage !== "reflector" || request.validateOutput === undefined) return null;
try {
request.validateOutput(output);
return null;
} catch (error) {
return error instanceof Error ? error.message : "Response validation failed";
}
}

function correctionMessage(reason: string): string {
return `The submitted response is invalid: ${reason}. Correct it and submit it again through ${RESPONSE_TOOL_NAME}.`;
}

function abortableDelay(delayMs: number, signal: AbortSignal): Promise<void> {
if (signal.aborted) return Promise.reject(signal.reason);
return new Promise((resolveDelay, reject) => {
Expand Down Expand Up @@ -155,6 +176,7 @@ class SessionPriorityGate implements InteractivePriorityGate {
}

export class CodingAgentEvolutionModelExecutor implements EvolutionModelExecutor {
readonly maxProviderCallsPerExecution = MAX_MODEL_ROUNDS * 2;
readonly #session: AgentSession;
readonly #modelRuntime: ModelRuntime;
readonly #priority: InteractivePriorityGate;
Expand Down Expand Up @@ -200,6 +222,7 @@ export class CodingAgentEvolutionModelExecutor implements EvolutionModelExecutor
timestamp: Date.now(),
},
];
let lastCorrection: string | null = null;
for (let round = 0; round < MAX_MODEL_ROUNDS; round += 1) {
request.signal?.throwIfAborted();
const waitStartedAt = performance.now();
Expand Down Expand Up @@ -247,11 +270,30 @@ export class CodingAgentEvolutionModelExecutor implements EvolutionModelExecutor
}
for (const call of calls) {
if (call.name === RESPONSE_TOOL_NAME) {
return {
output: call.arguments as JsonObject,
submissionMethod: "response-tool",
model: { provider: model.provider, model: model.id },
};
const output = call.arguments as JsonObject;
const validationError = validateEvolutionOutput(request, output);
if (validationError === null) {
return {
output,
submissionMethod: "response-tool",
model: { provider: model.provider, model: model.id },
};
}
lastCorrection = validationError;
messages.push({
role: "toolResult",
toolCallId: call.id,
toolName: call.name,
content: [
{
type: "text",
text: JSON.stringify(evolutionToolError(new Error(correctionMessage(validationError)))),
},
],
isError: true,
timestamp: Date.now(),
});
continue;
}
const tool = evolutionTools.find((candidate) => candidate.name === call.name);
let output: JsonObject;
Expand Down Expand Up @@ -280,20 +322,30 @@ export class CodingAgentEvolutionModelExecutor implements EvolutionModelExecutor
}
if (calls.length === 0) {
const parsed = parseJsonObject(contentText(assistant.content));
if (parsed) {
const validationError =
parsed === null ? "Response is not a JSON object" : validateEvolutionOutput(request, parsed);
if (parsed !== null && validationError === null) {
return {
output: parsed,
submissionMethod: "json-text",
model: { provider: model.provider, model: model.id },
};
}
lastCorrection = validationError;
messages.push({
role: "user",
content: `Return the result through ${RESPONSE_TOOL_NAME}.`,
content: correctionMessage(validationError ?? "Response validation failed"),
timestamp: Date.now(),
});
}
}
if (lastCorrection !== null) {
throw new EvolutionExecutionError(`Evolution model did not submit a valid response: ${lastCorrection}`, {
reasonCode: "reflection-invalid-response",
retryable: false,
diagnostics: { lastCorrection },
});
}
throw new Error(`Evolution model exceeded ${MAX_MODEL_ROUNDS} tool rounds`);
}

Expand Down
Loading
Loading