File size: 4,600 Bytes
d67f090
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
// Test tool-calling support across all configured models.
import { readFileSync } from "node:fs"
import { streamText, tool } from "ai"
import { createGoogleGenerativeAI } from "@ai-sdk/google"
import { createGroq } from "@ai-sdk/groq"
import { createOpenAI } from "@ai-sdk/openai"
import { z } from "zod"

// Tiny inline .env.local loader (avoids extra dep)
try {
  const env = readFileSync(".env.local", "utf8")
  for (const line of env.split("\n")) {
    const m = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*"?([^"\r\n]*)"?\s*$/)
    if (m && !process.env[m[1]]) process.env[m[1]] = m[2]
  }
} catch {}

const PROVIDERS = {
  google: {
    models: [
      "gemini-2.5-flash-lite",
      "gemini-2.5-flash",
      "gemini-2.5-pro",
      "gemini-2.0-flash",
      "gemini-2.0-flash-lite",
    ],
  },
  groq: {
    models: [
      "llama-3.3-70b-versatile",
      "llama-3.1-8b-instant",
      "openai/gpt-oss-120b",
      "openai/gpt-oss-20b",
      "qwen/qwen3-32b",
      "meta-llama/llama-4-scout-17b-16e-instruct",
    ],
  },
  github: {
    models: [
      "gpt-4o-mini",
      "gpt-4o",
      "Meta-Llama-3.1-8B-Instruct",
      "Meta-Llama-3.1-405B-Instruct",
    ],
  },
}

const tools = {
  calculator: tool({
    description: "Evaluate a math expression. Use for arithmetic.",
    parameters: z.object({ expression: z.string() }),
    execute: async ({ expression }) => {
      const r = Function(`"use strict";return (${expression})`)()
      return { result: r }
    },
  }),
  currentTime: tool({
    description: "Get current ISO date-time.",
    parameters: z.object({}),
    execute: async () => ({ now: new Date().toISOString() }),
  }),
}

function build(provider, modelId) {
  if (provider === "google")
    return createGoogleGenerativeAI({ apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY })(modelId)
  if (provider === "groq") return createGroq({ apiKey: process.env.GROQ_API_KEY })(modelId)
  if (provider === "github")
    return createOpenAI({
      apiKey: process.env.GITHUB_TOKEN,
      baseURL: "https://models.inference.ai.azure.com",
    })(modelId)
}

async function testOne(provider, modelId) {
  const t0 = Date.now()
  try {
    const result = streamText({
      model: build(provider, modelId),
      tools,
      maxSteps: 3,
      system:
        "Khi user hỏi tính toán PHẢI gọi tool calculator. " +
        "Khi user hỏi giờ PHẢI gọi tool currentTime. Không tự tính.",
      messages: [{ role: "user", content: "Tính giúp: 137 * 29 + 8 = ?" }],
    })

    let toolCalled = false
    let toolName = null
    let text = ""
    let chunks = 0
    let firstChunkMs = null

    for await (const part of result.fullStream) {
      chunks++
      if (firstChunkMs === null) firstChunkMs = Date.now() - t0
      if (part.type === "tool-call") {
        toolCalled = true
        toolName = part.toolName
      } else if (part.type === "text-delta") {
        text += part.textDelta
      } else if (part.type === "error") {
        throw part.error
      }
    }
    return {
      ok: true,
      toolCalled,
      toolName,
      streaming: chunks > 3,
      chunks,
      ttfbMs: firstChunkMs,
      totalMs: Date.now() - t0,
      textPreview: text.slice(0, 80).replace(/\s+/g, " "),
    }
  } catch (e) {
    return { ok: false, error: (e?.message || String(e)).slice(0, 240), totalMs: Date.now() - t0 }
  }
}

const results = []
for (const [provider, info] of Object.entries(PROVIDERS)) {
  for (const m of info.models) {
    process.stdout.write(`Testing ${provider}/${m} ... `)
    const r = await testOne(provider, m)
    const status = r.ok
      ? r.toolCalled
        ? `OK tool=${r.toolName} stream=${r.streaming ? "y" : "n"} chunks=${r.chunks} ttfb=${r.ttfbMs}ms total=${r.totalMs}ms`
        : `NO-TOOL stream=${r.streaming ? "y" : "n"} total=${r.totalMs}ms text="${r.textPreview}"`
      : `ERR ${r.error}`
    console.log(status)
    results.push({ provider, model: m, ...r })
    await new Promise((r) => setTimeout(r, 800))
  }
}

console.log("\n=== SUMMARY ===")
for (const r of results) {
  const tool = r.ok ? (r.toolCalled ? "OK " : "NO ") : "ERR"
  const stream = r.ok ? (r.streaming ? "y" : "n") : "-"
  console.log(`${tool} stream=${stream}  ${r.provider.padEnd(8)} ${r.model}`)
}
const broken = results.filter((r) => !r.ok)
const noTool = results.filter((r) => r.ok && !r.toolCalled)
console.log(`\n${results.length} tested · ${broken.length} broken · ${noTool.length} no tool-call`)
if (broken.length) {
  console.log("\nBROKEN:")
  for (const b of broken) console.log(`  ${b.provider}/${b.model}: ${b.error}`)
}