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 }}
|
- run: bun sst deploy --stage=${{ github.ref_name }}
|
||||||
env:
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
PLANETSCALE_SERVICE_TOKEN_NAME: ${{ secrets.PLANETSCALE_SERVICE_TOKEN_NAME }}
|
PLANETSCALE_SERVICE_TOKEN_NAME: ${{ secrets.PLANETSCALE_SERVICE_TOKEN_NAME }}
|
||||||
PLANETSCALE_SERVICE_TOKEN: ${{ secrets.PLANETSCALE_SERVICE_TOKEN }}
|
PLANETSCALE_SERVICE_TOKEN: ${{ secrets.PLANETSCALE_SERVICE_TOKEN }}
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ function sdkKey(npm: string): string | undefined {
|
|||||||
return "gateway"
|
return "gateway"
|
||||||
case "@openrouter/ai-sdk-provider":
|
case "@openrouter/ai-sdk-provider":
|
||||||
return "openrouter"
|
return "openrouter"
|
||||||
|
case "merge-gateway-ai-sdk-provider":
|
||||||
|
return "mergeGateway"
|
||||||
case "ai-gateway-provider":
|
case "ai-gateway-provider":
|
||||||
// ai-gateway-provider/unified wraps createOpenAICompatible({ name: "Unified" }),
|
// ai-gateway-provider/unified wraps createOpenAICompatible({ name: "Unified" }),
|
||||||
// and @ai-sdk/openai-compatible parses compatibleOptions from one of
|
// 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 "@ai-sdk/togetherai":
|
||||||
case "venice-ai-sdk-provider":
|
case "venice-ai-sdk-provider":
|
||||||
case "ai-gateway-provider":
|
case "ai-gateway-provider":
|
||||||
|
case "merge-gateway-ai-sdk-provider":
|
||||||
return { reasoningEffort: effort }
|
return { reasoningEffort: effort }
|
||||||
case "@ai-sdk/cohere":
|
case "@ai-sdk/cohere":
|
||||||
case "@ai-sdk/perplexity":
|
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)
|
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", () => {
|
test("public provider info omits invalid models", () => {
|
||||||
const provider = Provider.fromModelsDevProvider({
|
const provider = Provider.fromModelsDevProvider({
|
||||||
id: "test",
|
id: "test",
|
||||||
|
|||||||
@@ -3370,6 +3370,7 @@ describe("ProviderTransform.reasoningVariants", () => {
|
|||||||
["@ai-sdk/togetherai", { reasoningEffort: "high" }],
|
["@ai-sdk/togetherai", { reasoningEffort: "high" }],
|
||||||
["venice-ai-sdk-provider", { reasoningEffort: "high" }],
|
["venice-ai-sdk-provider", { reasoningEffort: "high" }],
|
||||||
["ai-gateway-provider", { reasoningEffort: "high" }],
|
["ai-gateway-provider", { reasoningEffort: "high" }],
|
||||||
|
["merge-gateway-ai-sdk-provider", { reasoningEffort: "high" }],
|
||||||
["@ai-sdk/amazon-bedrock", { reasoningConfig: { type: "enabled", maxReasoningEffort: "high" } }],
|
["@ai-sdk/amazon-bedrock", { reasoningConfig: { type: "enabled", maxReasoningEffort: "high" } }],
|
||||||
])("converts effort for %s", (npm, expected, ...args) => {
|
])("converts effort for %s", (npm, expected, ...args) => {
|
||||||
const id = args[0] as string | undefined
|
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", () => {
|
describe("ProviderTransform.options - kimi family adaptive thinking", () => {
|
||||||
const createModel = (overrides: Record<string, any> = {}) =>
|
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("'geo_model' ELSE 'geo'")
|
||||||
expect(queries[1]).toContain("0 AS sessions")
|
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) {
|
function aggregate(model: string, provider: string) {
|
||||||
|
|||||||
@@ -10,7 +10,14 @@ import {
|
|||||||
statProvider,
|
statProvider,
|
||||||
} from "./model-normalization"
|
} from "./model-normalization"
|
||||||
import type { ProviderStatAggregate } from "./provider"
|
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 StatDimension = "model" | "provider" | "geo" | "geo_model"
|
||||||
export type StatsQuerySource = { namespace: string; table: string; dataset: string }
|
export type StatsQuerySource = { namespace: string; table: string; dataset: string }
|
||||||
@@ -18,6 +25,10 @@ type StatsQueryFamily = "usage" | "geo"
|
|||||||
|
|
||||||
const DAY_MS = 86_400_000
|
const DAY_MS = 86_400_000
|
||||||
const WEEK_MS = 7 * DAY_MS
|
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
|
// 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
|
// queries per day/week keep each result bounded and avoid combining the costly
|
||||||
@@ -123,6 +134,10 @@ WITH normalized AS (
|
|||||||
FROM ${sourceTable}
|
FROM ${sourceTable}
|
||||||
WHERE event_type = 'generation.completed'
|
WHERE event_type = 'generation.completed'
|
||||||
AND source IN ('inference', 'inference-legacy')
|
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 product = 'go'
|
||||||
AND model_requested IS NOT NULL
|
AND model_requested IS NOT NULL
|
||||||
AND model_requested <> ''
|
AND model_requested <> ''
|
||||||
@@ -264,27 +279,19 @@ function sqlString(value: string) {
|
|||||||
|
|
||||||
function statPeriods(grain: "day" | "week", periodStart: Date, periodEnd: Date) {
|
function statPeriods(grain: "day" | "week", periodStart: Date, periodEnd: Date) {
|
||||||
const interval = grain === "day" ? DAY_MS : WEEK_MS
|
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) => {
|
return Array.from({ length: count }, (_, index) => {
|
||||||
const start = new Date(periodStart.getTime() + index * interval)
|
const start = new Date(first.getTime() + index * interval)
|
||||||
return {
|
return {
|
||||||
grain,
|
grain,
|
||||||
key: grain === "day" ? start.toISOString().slice(0, 10) : isoWeekKey(start),
|
key: periodKeyFor(grain, start),
|
||||||
start,
|
start,
|
||||||
end: new Date(Math.min(start.getTime() + interval, periodEnd.getTime())),
|
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) {
|
function statModelSql(model: string, providerModel: string) {
|
||||||
return `COALESCE(NULLIF(regexp_replace(CASE
|
return `COALESCE(NULLIF(regexp_replace(CASE
|
||||||
WHEN lower(${model}) = 'big-pickle' THEN NULLIF(${providerModel}, '')
|
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 |
|
| 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-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-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 |
|
| [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
|
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
|
||||||
|
|
||||||
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:
|
OpenCode supports two authentication methods:
|
||||||
|
|
||||||
@@ -2487,7 +2487,7 @@ You can use any OpenAI-compatible provider with opencode. Most modern AI provide
|
|||||||
"provider": {
|
"provider": {
|
||||||
"myprovider": {
|
"myprovider": {
|
||||||
"npm": "@ai-sdk/openai-compatible",
|
"npm": "@ai-sdk/openai-compatible",
|
||||||
"name": "My AI ProviderDisplay Name",
|
"name": "My AI Provider Display Name",
|
||||||
"options": {
|
"options": {
|
||||||
"baseURL": "https://api.myprovider.com/v1"
|
"baseURL": "https://api.myprovider.com/v1"
|
||||||
},
|
},
|
||||||
@@ -2525,7 +2525,7 @@ Here's an example setting the `apiKey`, `headers`, and model `limit` options.
|
|||||||
"provider": {
|
"provider": {
|
||||||
"myprovider": {
|
"myprovider": {
|
||||||
"npm": "@ai-sdk/openai-compatible",
|
"npm": "@ai-sdk/openai-compatible",
|
||||||
"name": "My AI ProviderDisplay Name",
|
"name": "My AI Provider Display Name",
|
||||||
"options": {
|
"options": {
|
||||||
"baseURL": "https://api.myprovider.com/v1",
|
"baseURL": "https://api.myprovider.com/v1",
|
||||||
"apiKey": "{env:ANTHROPIC_API_KEY}",
|
"apiKey": "{env:ANTHROPIC_API_KEY}",
|
||||||
|
|||||||
Reference in New Issue
Block a user