feat: agents and skills

This commit is contained in:
Gab
2026-03-24 17:42:26 +11:00
parent 485cc7649e
commit ff77a81141
10 changed files with 305 additions and 13 deletions

View File

@@ -15,6 +15,13 @@ export namespace ModelsDev {
const log = Log.create({ service: "models.dev" })
const filepath = path.join(Global.Path.cache, "models.json")
const REGION_URLS: Record<string, string> = {
dev: "https://ai.toothfairylab.link",
au: "https://ai.toothfairyai.com",
eu: "https://ai.eu.toothfairyai.com",
us: "https://ai.us.toothfairyai.com",
}
export const Model = z.object({
id: z.string(),
name: z.string(),
@@ -81,6 +88,21 @@ export namespace ModelsDev {
export type Provider = z.infer<typeof Provider>
interface TFModel {
name: string
provider: string
modelType: string
reasoner: boolean
supportsVision: boolean
toolCalling: boolean
maxTokens: number
deprecated: boolean
pricing?: {
inputPer1mTokens: number
outputPer1mTokens: number
}
}
function url() {
return Flag.OPENCODE_MODELS_URL || "https://models.dev"
}
@@ -100,7 +122,122 @@ export namespace ModelsDev {
export async function get() {
const result = await Data()
return result as Record<string, Provider>
const providers = result as Record<string, Provider>
// Try to fetch ToothFairyAI models dynamically
// First check env vars, then stored credentials
let tfApiKey = process.env.TF_API_KEY
let tfRegion = process.env.TF_REGION || "au"
// Try to load from stored credentials
if (!tfApiKey) {
try {
const credPath = path.join(Global.Path.data, ".tfcode", "credentials.json")
const credData = await Bun.file(credPath).json() as { api_key?: string; region?: string }
if (credData.api_key) {
tfApiKey = credData.api_key
tfRegion = credData.region || "au"
}
} catch {}
}
const tfBaseUrl = REGION_URLS[tfRegion] || REGION_URLS.au
if (tfApiKey) {
try {
const tfResponse = await fetch(`${tfBaseUrl}/models_list`, {
headers: {
"x-api-key": tfApiKey,
},
signal: AbortSignal.timeout(10000),
})
if (tfResponse.ok) {
const tfData = await tfResponse.json() as { templates: Record<string, TFModel> }
const tfModels: Record<string, Model> = {}
for (const [key, model] of Object.entries(tfData.templates || {})) {
if (model.deprecated) continue
const modelId = key.startsWith("z/") ? key.slice(2) : key
tfModels[modelId] = {
id: modelId,
name: model.name,
family: model.provider,
release_date: new Date().toISOString().split("T")[0],
attachment: model.supportsVision || false,
reasoning: model.reasoner || false,
temperature: true,
tool_call: model.toolCalling || false,
options: {},
cost: {
input: model.pricing?.inputPer1mTokens || 0,
output: model.pricing?.outputPer1mTokens || 0,
},
limit: {
context: model.maxTokens ? model.maxTokens * 4 : 128000,
output: model.maxTokens || 16000,
},
modalities: {
input: model.supportsVision ? ["text", "image"] : ["text"],
output: ["text"],
},
}
}
providers["toothfairyai"] = {
id: "toothfairyai",
name: "ToothFairyAI",
env: ["TF_API_KEY", "TF_WORKSPACE_ID"],
models: tfModels,
}
}
} catch (e) {
log.error("Failed to fetch ToothFairyAI models", { error: e })
}
}
// Fallback to static models if dynamic fetch failed
if (!providers["toothfairyai"] || Object.keys(providers["toothfairyai"].models).length === 0) {
providers["toothfairyai"] = {
id: "toothfairyai",
name: "ToothFairyAI",
env: ["TF_API_KEY", "TF_WORKSPACE_ID"],
models: {
"sorcerer": {
id: "sorcerer",
name: "TF Sorcerer",
family: "groq",
release_date: "2025-01-01",
attachment: true,
reasoning: false,
temperature: true,
tool_call: true,
options: {},
cost: { input: 0.62, output: 1.75 },
limit: { context: 128000, output: 16000 },
modalities: { input: ["text", "image"], output: ["text"] },
},
"mystica": {
id: "mystica",
name: "TF Mystica",
family: "fireworks",
release_date: "2025-01-01",
attachment: true,
reasoning: false,
temperature: true,
tool_call: true,
options: {},
cost: { input: 2.5, output: 7.5 },
limit: { context: 128000, output: 16000 },
modalities: { input: ["text", "image"], output: ["text"] },
},
},
}
}
return providers
}
export async function refresh() {

View File

@@ -182,6 +182,29 @@ export namespace Provider {
options: hasKey ? {} : { apiKey: "public" },
}
},
async toothfairyai(input) {
const hasCredentials = await (async () => {
const env = Env.all()
if (env.TF_API_KEY) return true
if (await Auth.get(input.id)) return true
const config = await Config.get()
if (config.provider?.["toothfairyai"]?.options?.apiKey) return true
// Check stored credentials file
try {
const credPath = path.join(Global.Path.data, ".tfcode", "credentials.json")
const credData = await Bun.file(credPath).json() as { api_key?: string }
if (credData.api_key) return true
} catch {}
return false
})()
return {
autoload: hasCredentials,
options: hasCredentials ? {} : { apiKey: "setup-required" },
}
},
openai: async () => {
return {
autoload: false,

View File

@@ -12,6 +12,7 @@ export const ProviderID = providerIdSchema.pipe(
make: (id: string) => schema.makeUnsafe(id),
zod: z.string().pipe(z.custom<ProviderID>()),
// Well-known providers
toothfairyai: schema.makeUnsafe("toothfairyai"),
opencode: schema.makeUnsafe("opencode"),
anthropic: schema.makeUnsafe("anthropic"),
openai: schema.makeUnsafe("openai"),