Przeglądaj źródła

Merge branch 'dev' of github.com:anomalyco/opencode into dev

Frank 3 dni temu
rodzic
commit
069165eb5b

+ 1 - 0
.github/workflows/deploy.yml

@@ -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 }}

+ 3 - 0
packages/opencode/src/provider/transform.ts

@@ -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":

+ 27 - 0
packages/opencode/test/provider/provider.test.ts

@@ -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",

+ 20 - 0
packages/opencode/test/provider/transform.test.ts

@@ -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> = {}) =>
     ({

+ 28 - 0
packages/stats/core/src/domain/inference.test.ts

@@ -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) {

+ 20 - 13
packages/stats/core/src/domain/inference.ts

@@ -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}, '')

+ 1 - 1
packages/web/src/content/docs/ecosystem.mdx

@@ -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                                      |

+ 1 - 1
packages/web/src/content/docs/github.mdx

@@ -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.
 
 ---
 

+ 3 - 3
packages/web/src/content/docs/providers.mdx

@@ -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}",