Move to threads based system.

This commit is contained in:
Gabriel Brown
2026-06-22 10:37:26 -04:00
parent 8ae6c4b533
commit 206b64176b
82 changed files with 6169 additions and 1930 deletions
+86
View File
@@ -0,0 +1,86 @@
import 'server-only';
import { NextResponse } from 'next/server';
import { env } from '@/env';
import { convexAuthNextjsToken } from '@convex-dev/auth/nextjs/server';
import { fetchQuery } from 'convex/nextjs';
import type { Id } from '@spoon/backend/convex/_generated/dataModel.js';
import { api } from '@spoon/backend/convex/_generated/api.js';
type RouteContext = {
params: Promise<{ jobId: string }> | { jobId: string };
};
export const routeJobId = async (context: RouteContext) => {
const params = await context.params;
return params.jobId as Id<'agentJobs'>;
};
const workerToken = () =>
env.SPOON_AGENT_WORKER_INTERNAL_TOKEN ?? env.SPOON_WORKER_TOKEN;
export const requireOwnedJob = async (jobId: Id<'agentJobs'>) => {
const token = await convexAuthNextjsToken();
if (!token) {
return {
ok: false as const,
response: NextResponse.json({ error: 'Unauthorized' }, { status: 401 }),
};
}
await fetchQuery(api.agentJobs.assertOwned, { jobId }, { token });
return { ok: true as const };
};
export const proxyWorker = async (
jobId: Id<'agentJobs'>,
action: string,
init?: RequestInit,
search?: URLSearchParams,
) => {
const token = workerToken();
if (!token) {
return NextResponse.json(
{ error: 'SPOON_AGENT_WORKER_INTERNAL_TOKEN is not configured.' },
{ status: 500 },
);
}
const url = new URL(
`/jobs/${encodeURIComponent(jobId)}/${action}`,
env.SPOON_AGENT_WORKER_URL,
);
if (search) {
for (const [key, value] of search) url.searchParams.set(key, value);
}
const response = await fetch(url, {
...init,
headers: {
authorization: `Bearer ${token}`,
'content-type': 'application/json',
...init?.headers,
},
});
const text = await response.text();
return new NextResponse(text, {
status: response.status,
headers: {
'content-type':
response.headers.get('content-type') ?? 'application/json',
},
});
};
export const withOwnedJob = async (
context: RouteContext,
handler: (jobId: Id<'agentJobs'>) => Promise<Response>,
) => {
try {
const jobId = await routeJobId(context);
const owned = await requireOwnedJob(jobId);
if (!owned.ok) return owned.response;
return await handler(jobId);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return NextResponse.json({ error: message }, { status: 500 });
}
};
+56
View File
@@ -0,0 +1,56 @@
type ModelsDevModel = {
id?: string;
name?: string;
tool_call?: boolean;
reasoning?: boolean;
limit?: { context?: number };
};
type ModelsDevProvider = {
id?: string;
name?: string;
models?: Record<string, ModelsDevModel>;
};
const providerMap = {
openai: 'openai',
anthropic: 'anthropic',
google: 'google',
openrouter: 'openrouter',
requesty: 'requesty',
litellm: 'litellm',
cloudflare_ai_gateway: 'cloudflare',
custom_openai_compatible: '',
opencode_openai_login: 'openai',
} as const;
export type ProviderModelOption = {
id: string;
label: string;
reasoning: boolean;
toolCall: boolean;
context?: number;
};
export const loadModelsDevOptions = async (provider: string) => {
const mapped = providerMap[provider as keyof typeof providerMap];
if (!mapped) return [];
const response = await fetch('https://models.dev/api.json', {
cache: 'force-cache',
});
if (!response.ok) return [];
const catalog = (await response.json()) as Record<string, ModelsDevProvider>;
const providerCatalog = catalog[mapped];
return Object.entries(providerCatalog?.models ?? {})
.map(
([id, model]): ProviderModelOption => ({
id: model.id ?? id,
label: model.name ?? model.id ?? id,
reasoning: Boolean(model.reasoning),
toolCall: Boolean(model.tool_call),
context: model.limit?.context,
}),
)
.filter((model) => model.toolCall)
.sort((a, b) => a.label.localeCompare(b.label));
};