Merge branch 'dev' of github.com:anomalyco/opencode into dev
This commit is contained in:
@@ -35,6 +35,7 @@ jobs:
|
||||
|
||||
- run: bun sst deploy --stage=${{ github.ref_name }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
PLANETSCALE_SERVICE_TOKEN_NAME: ${{ secrets.PLANETSCALE_SERVICE_TOKEN_NAME }}
|
||||
PLANETSCALE_SERVICE_TOKEN: ${{ secrets.PLANETSCALE_SERVICE_TOKEN }}
|
||||
|
||||
@@ -84,6 +84,8 @@ function sdkKey(npm: string): string | undefined {
|
||||
return "gateway"
|
||||
case "@openrouter/ai-sdk-provider":
|
||||
return "openrouter"
|
||||
case "merge-gateway-ai-sdk-provider":
|
||||
return "mergeGateway"
|
||||
case "ai-gateway-provider":
|
||||
// ai-gateway-provider/unified wraps createOpenAICompatible({ name: "Unified" }),
|
||||
// and @ai-sdk/openai-compatible parses compatibleOptions from one of
|
||||
@@ -1772,6 +1774,7 @@ function reasoningEffort(model: Provider.Model, effort: string) {
|
||||
case "@ai-sdk/togetherai":
|
||||
case "venice-ai-sdk-provider":
|
||||
case "ai-gateway-provider":
|
||||
case "merge-gateway-ai-sdk-provider":
|
||||
return { reasoningEffort: effort }
|
||||
case "@ai-sdk/cohere":
|
||||
case "@ai-sdk/perplexity":
|
||||
|
||||
@@ -1548,6 +1548,33 @@ test("models.dev reasoning options replace generated variants and unsupported to
|
||||
expect(models["gemini-3-pro-fast"].variants).toEqual(models.override.variants)
|
||||
})
|
||||
|
||||
test("MERGE Gateway exposes declared effort variants without model-specific handling", () => {
|
||||
const provider = {
|
||||
id: "merge-gateway",
|
||||
name: "MERGE Gateway",
|
||||
env: ["MERGE_GATEWAY_API_KEY"],
|
||||
npm: "merge-gateway-ai-sdk-provider",
|
||||
models: {
|
||||
"openai/gpt-5.6-sol": {
|
||||
id: "openai/gpt-5.6-sol",
|
||||
name: "GPT-5.6 Sol",
|
||||
reasoning: true,
|
||||
reasoning_options: [{ type: "effort", values: ["none", "low", "medium", "high", "xhigh", "max"] }],
|
||||
limit: { context: 128_000, output: 64_000 },
|
||||
},
|
||||
},
|
||||
} as unknown as ModelsDev.Provider
|
||||
|
||||
expect(Provider.fromModelsDevProvider(provider).models["openai/gpt-5.6-sol"].variants).toEqual({
|
||||
none: { reasoningEffort: "none" },
|
||||
low: { reasoningEffort: "low" },
|
||||
medium: { reasoningEffort: "medium" },
|
||||
high: { reasoningEffort: "high" },
|
||||
xhigh: { reasoningEffort: "xhigh" },
|
||||
max: { reasoningEffort: "max" },
|
||||
})
|
||||
})
|
||||
|
||||
test("public provider info omits invalid models", () => {
|
||||
const provider = Provider.fromModelsDevProvider({
|
||||
id: "test",
|
||||
|
||||
@@ -3370,6 +3370,7 @@ describe("ProviderTransform.reasoningVariants", () => {
|
||||
["@ai-sdk/togetherai", { reasoningEffort: "high" }],
|
||||
["venice-ai-sdk-provider", { reasoningEffort: "high" }],
|
||||
["ai-gateway-provider", { reasoningEffort: "high" }],
|
||||
["merge-gateway-ai-sdk-provider", { reasoningEffort: "high" }],
|
||||
["@ai-sdk/amazon-bedrock", { reasoningConfig: { type: "enabled", maxReasoningEffort: "high" } }],
|
||||
])("converts effort for %s", (npm, expected, ...args) => {
|
||||
const id = args[0] as string | undefined
|
||||
@@ -5555,6 +5556,25 @@ describe("ProviderTransform.providerOptions - ai-gateway-provider", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("ProviderTransform.providerOptions - merge-gateway-ai-sdk-provider", () => {
|
||||
const model = {
|
||||
id: "merge-gateway/openai/gpt-5.6-sol",
|
||||
providerID: "merge-gateway",
|
||||
api: {
|
||||
id: "openai/gpt-5.6-sol",
|
||||
url: "https://api-gateway.merge.dev/v1/ai-sdk",
|
||||
npm: "merge-gateway-ai-sdk-provider",
|
||||
},
|
||||
capabilities: { reasoning: true },
|
||||
} as any
|
||||
|
||||
test("routes normalized effort under the adapter's mergeGateway key", () => {
|
||||
expect(ProviderTransform.providerOptions(model, { reasoningEffort: "high" })).toEqual({
|
||||
mergeGateway: { reasoningEffort: "high" },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("ProviderTransform.options - kimi family adaptive thinking", () => {
|
||||
const createModel = (overrides: Record<string, any> = {}) =>
|
||||
({
|
||||
|
||||
@@ -103,6 +103,34 @@ describe("inference stat normalization", () => {
|
||||
expect(queries[1]).toContain("'geo_model' ELSE 'geo'")
|
||||
expect(queries[1]).toContain("0 AS sessions")
|
||||
})
|
||||
|
||||
test("aligns periods to UTC calendar boundaries", () => {
|
||||
const queries = buildStatsQueries(new Date("2026-06-17T15:56:00.000Z"), new Date("2026-06-19T15:56:00.000Z"), {
|
||||
namespace: "inference",
|
||||
table: "generation",
|
||||
dataset: "zen",
|
||||
})
|
||||
|
||||
expect(queries).toHaveLength(8)
|
||||
expect(queries[0]).toContain("'2026-W25' AS period_key")
|
||||
expect(queries[0]).toContain("started_at >= '2026-06-15T00:00:00.000Z'")
|
||||
expect(queries[2]).toContain("'2026-06-17' AS period_key")
|
||||
expect(queries[2]).toContain("started_at >= '2026-06-17T00:00:00.000Z'")
|
||||
expect(queries[2]).toContain("started_at < '2026-06-18T00:00:00.000Z'")
|
||||
expect(queries[6]).toContain("'2026-06-19' AS period_key")
|
||||
expect(queries[6]).toContain("started_at < '2026-06-19T15:56:00.000Z'")
|
||||
})
|
||||
|
||||
test("uses an exclusive live and legacy source handoff", () => {
|
||||
const [query] = buildStatsQueries(new Date("2026-08-11T00:00:00.000Z"), new Date("2026-08-12T00:00:00.000Z"), {
|
||||
namespace: "inference",
|
||||
table: "generation",
|
||||
dataset: "zen",
|
||||
})
|
||||
|
||||
expect(query).toContain("(source = 'inference-legacy' AND started_at < '2026-08-11T10:57:48.186Z')")
|
||||
expect(query).toContain("(source = 'inference' AND started_at >= '2026-08-11T10:57:48.186Z')")
|
||||
})
|
||||
})
|
||||
|
||||
function aggregate(model: string, provider: string) {
|
||||
|
||||
@@ -10,7 +10,14 @@ import {
|
||||
statProvider,
|
||||
} from "./model-normalization"
|
||||
import type { ProviderStatAggregate } from "./provider"
|
||||
import { normalizeCountry, normalizeTier, type StatBaseAggregate } from "./stat"
|
||||
import {
|
||||
normalizeCountry,
|
||||
normalizeTier,
|
||||
periodKeyFor,
|
||||
startOfIsoWeek,
|
||||
startOfUtcDay,
|
||||
type StatBaseAggregate,
|
||||
} from "./stat"
|
||||
|
||||
export type StatDimension = "model" | "provider" | "geo" | "geo_model"
|
||||
export type StatsQuerySource = { namespace: string; table: string; dataset: string }
|
||||
@@ -18,6 +25,10 @@ type StatsQueryFamily = "usage" | "geo"
|
||||
|
||||
const DAY_MS = 86_400_000
|
||||
const WEEK_MS = 7 * DAY_MS
|
||||
// The typed production stream began before the legacy backfill's original end
|
||||
// boundary. Use one exclusive handoff so the overlapping rows are never counted
|
||||
// from both sources.
|
||||
const LIVE_SOURCE_START = "2026-08-11T10:57:48.186Z"
|
||||
|
||||
// R2 SQL limits result sets to 10,000 rows and does not support OFFSET. Two
|
||||
// queries per day/week keep each result bounded and avoid combining the costly
|
||||
@@ -123,6 +134,10 @@ WITH normalized AS (
|
||||
FROM ${sourceTable}
|
||||
WHERE event_type = 'generation.completed'
|
||||
AND source IN ('inference', 'inference-legacy')
|
||||
AND (
|
||||
(source = 'inference-legacy' AND started_at < ${sqlString(LIVE_SOURCE_START)})
|
||||
OR (source = 'inference' AND started_at >= ${sqlString(LIVE_SOURCE_START)})
|
||||
)
|
||||
AND product = 'go'
|
||||
AND model_requested IS NOT NULL
|
||||
AND model_requested <> ''
|
||||
@@ -264,27 +279,19 @@ function sqlString(value: string) {
|
||||
|
||||
function statPeriods(grain: "day" | "week", periodStart: Date, periodEnd: Date) {
|
||||
const interval = grain === "day" ? DAY_MS : WEEK_MS
|
||||
const count = Math.max(0, Math.ceil((periodEnd.getTime() - periodStart.getTime()) / interval))
|
||||
const first = grain === "day" ? startOfUtcDay(periodStart) : startOfIsoWeek(periodStart)
|
||||
const count = Math.max(0, Math.ceil((periodEnd.getTime() - first.getTime()) / interval))
|
||||
return Array.from({ length: count }, (_, index) => {
|
||||
const start = new Date(periodStart.getTime() + index * interval)
|
||||
const start = new Date(first.getTime() + index * interval)
|
||||
return {
|
||||
grain,
|
||||
key: grain === "day" ? start.toISOString().slice(0, 10) : isoWeekKey(start),
|
||||
key: periodKeyFor(grain, start),
|
||||
start,
|
||||
end: new Date(Math.min(start.getTime() + interval, periodEnd.getTime())),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function isoWeekKey(date: Date) {
|
||||
const thursday = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()))
|
||||
const day = thursday.getUTCDay() || 7
|
||||
thursday.setUTCDate(thursday.getUTCDate() + 4 - day)
|
||||
const year = thursday.getUTCFullYear()
|
||||
const week = Math.ceil((thursday.getTime() - Date.UTC(year, 0, 1) + DAY_MS) / WEEK_MS)
|
||||
return `${year}-W${String(week).padStart(2, "0")}`
|
||||
}
|
||||
|
||||
function statModelSql(model: string, providerModel: string) {
|
||||
return `COALESCE(NULLIF(regexp_replace(CASE
|
||||
WHEN lower(${model}) = 'big-pickle' THEN NULLIF(${providerModel}, '')
|
||||
|
||||
@@ -17,7 +17,7 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw
|
||||
|
||||
| Name | Description |
|
||||
| -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
||||
| [opencode-daytona](https://github.com/daytonaio/daytona/tree/main/libs/opencode-plugin) | Automatically run OpenCode sessions in isolated Daytona sandboxes with git sync and live previews |
|
||||
| [opencode-daytona](https://github.com/daytona/integrations/tree/main/packages/opencode-plugin) | Automatically run OpenCode sessions in isolated Daytona sandboxes with git sync and live previews |
|
||||
| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Automatically inject Helicone session headers for request grouping |
|
||||
| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Auto-inject TypeScript/Svelte types into file reads with lookup tools |
|
||||
| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Use your ChatGPT Plus/Pro subscription instead of API credits |
|
||||
|
||||
@@ -97,7 +97,7 @@ Or you can set it up manually.
|
||||
issues: write
|
||||
```
|
||||
|
||||
You can also use a [personal access tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)(PAT) if preferred.
|
||||
You can also use a [personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)(PAT) if preferred.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -759,7 +759,7 @@ Cloudflare Workers AI lets you run AI models on Cloudflare's global network dire
|
||||
|
||||
### DigitalOcean
|
||||
|
||||
DigitalOcean's [Inference Engine](https://docs.digitalocean.com/products/inference/) provides access to open models like GPT-OSS, Llama, Qwen, and DeepSeek, plus custom [Inference Routers](https://docs.digitalocean.com/products/genai-platform/concepts/inference-routers/) that route each request to the cheapest, fastest, or best-fit model for a task.
|
||||
DigitalOcean's [Inference Engine](https://docs.digitalocean.com/products/inference/) provides access to open models like GPT-OSS, Llama, Qwen, and DeepSeek, plus custom [Inference Routers](https://docs.digitalocean.com/products/inference/how-to/use-inference-router/) that route each request to the cheapest, fastest, or best-fit model for a task.
|
||||
|
||||
OpenCode supports two authentication methods:
|
||||
|
||||
@@ -2487,7 +2487,7 @@ You can use any OpenAI-compatible provider with opencode. Most modern AI provide
|
||||
"provider": {
|
||||
"myprovider": {
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"name": "My AI ProviderDisplay Name",
|
||||
"name": "My AI Provider Display Name",
|
||||
"options": {
|
||||
"baseURL": "https://api.myprovider.com/v1"
|
||||
},
|
||||
@@ -2525,7 +2525,7 @@ Here's an example setting the `apiKey`, `headers`, and model `limit` options.
|
||||
"provider": {
|
||||
"myprovider": {
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"name": "My AI ProviderDisplay Name",
|
||||
"name": "My AI Provider Display Name",
|
||||
"options": {
|
||||
"baseURL": "https://api.myprovider.com/v1",
|
||||
"apiKey": "{env:ANTHROPIC_API_KEY}",
|
||||
|
||||
Reference in New Issue
Block a user