mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-19 16:54:22 +01:00
Zen: telemetry
This commit is contained in:
@@ -22,7 +22,7 @@ const MODELS = {
|
|||||||
// headerMappings: {},
|
// headerMappings: {},
|
||||||
// },
|
// },
|
||||||
"qwen/qwen3-coder": {
|
"qwen/qwen3-coder": {
|
||||||
id: "qwen/qwen3-coder",
|
id: "qwen/qwen3-coder" as const,
|
||||||
auth: true,
|
auth: true,
|
||||||
api: "https://inference.baseten.co",
|
api: "https://inference.baseten.co",
|
||||||
apiKey: Resource.BASETEN_API_KEY.value,
|
apiKey: Resource.BASETEN_API_KEY.value,
|
||||||
@@ -37,7 +37,7 @@ const MODELS = {
|
|||||||
headerMappings: {},
|
headerMappings: {},
|
||||||
},
|
},
|
||||||
"grok-code": {
|
"grok-code": {
|
||||||
id: "x-ai/grok-code-fast-1",
|
id: "x-ai/grok-code-fast-1" as const,
|
||||||
auth: false,
|
auth: false,
|
||||||
api: "https://api.x.ai",
|
api: "https://api.x.ai",
|
||||||
apiKey: Resource.XAI_API_KEY.value,
|
apiKey: Resource.XAI_API_KEY.value,
|
||||||
@@ -68,6 +68,11 @@ export async function POST(input: APIEvent) {
|
|||||||
try {
|
try {
|
||||||
const url = new URL(input.request.url)
|
const url = new URL(input.request.url)
|
||||||
const body = await input.request.json()
|
const body = await input.request.json()
|
||||||
|
logMetric({
|
||||||
|
is_tream: !!body.stream,
|
||||||
|
session: input.request.headers.get("x-opencode-session"),
|
||||||
|
request: input.request.headers.get("x-opencode-request"),
|
||||||
|
})
|
||||||
const MODEL = validateModel()
|
const MODEL = validateModel()
|
||||||
const apiKey = await authenticate()
|
const apiKey = await authenticate()
|
||||||
const isFree = FREE_WORKSPACES.includes(apiKey?.workspaceID ?? "")
|
const isFree = FREE_WORKSPACES.includes(apiKey?.workspaceID ?? "")
|
||||||
@@ -106,9 +111,11 @@ export async function POST(input: APIEvent) {
|
|||||||
|
|
||||||
// Handle non-streaming response
|
// Handle non-streaming response
|
||||||
if (!body.stream) {
|
if (!body.stream) {
|
||||||
const body = await res.json()
|
const json = await res.json()
|
||||||
await trackUsage(body)
|
const body = JSON.stringify(json)
|
||||||
return new Response(JSON.stringify(body), {
|
logMetric({ response_length: body.length })
|
||||||
|
await trackUsage(json)
|
||||||
|
return new Response(body, {
|
||||||
status: res.status,
|
status: res.status,
|
||||||
statusText: res.statusText,
|
statusText: res.statusText,
|
||||||
headers: resHeaders,
|
headers: resHeaders,
|
||||||
@@ -121,16 +128,26 @@ export async function POST(input: APIEvent) {
|
|||||||
const reader = res.body?.getReader()
|
const reader = res.body?.getReader()
|
||||||
const decoder = new TextDecoder()
|
const decoder = new TextDecoder()
|
||||||
let buffer = ""
|
let buffer = ""
|
||||||
|
let responseLength = 0
|
||||||
|
let startTimestamp = Date.now()
|
||||||
|
let receivedFirstByte = false
|
||||||
|
|
||||||
function pump(): Promise<void> {
|
function pump(): Promise<void> {
|
||||||
return (
|
return (
|
||||||
reader?.read().then(async ({ done, value }) => {
|
reader?.read().then(async ({ done, value }) => {
|
||||||
if (done) {
|
if (done) {
|
||||||
|
logMetric({ response_length: responseLength })
|
||||||
c.close()
|
c.close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!receivedFirstByte) {
|
||||||
|
receivedFirstByte = true
|
||||||
|
logMetric({ time_to_first_byte: Date.now() - startTimestamp })
|
||||||
|
}
|
||||||
|
|
||||||
buffer += decoder.decode(value, { stream: true })
|
buffer += decoder.decode(value, { stream: true })
|
||||||
|
responseLength += value.length
|
||||||
|
|
||||||
const parts = buffer.split("\n\n")
|
const parts = buffer.split("\n\n")
|
||||||
buffer = parts.pop() ?? ""
|
buffer = parts.pop() ?? ""
|
||||||
@@ -169,7 +186,9 @@ export async function POST(input: APIEvent) {
|
|||||||
if (!(body.model in MODELS)) {
|
if (!(body.model in MODELS)) {
|
||||||
throw new ModelError(`Model ${body.model} not supported`)
|
throw new ModelError(`Model ${body.model} not supported`)
|
||||||
}
|
}
|
||||||
return MODELS[body.model as keyof typeof MODELS]
|
const model = MODELS[body.model as keyof typeof MODELS]
|
||||||
|
logMetric({ model: model.id })
|
||||||
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
async function authenticate() {
|
async function authenticate() {
|
||||||
@@ -190,9 +209,12 @@ export async function POST(input: APIEvent) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (!key) throw new AuthError("Invalid API key.")
|
if (!key) throw new AuthError("Invalid API key.")
|
||||||
|
logMetric({
|
||||||
|
api_key: key.id,
|
||||||
|
workspace: key.workspaceID,
|
||||||
|
})
|
||||||
return key
|
return key
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e)
|
|
||||||
// ignore error if model does not require authentication
|
// ignore error if model does not require authentication
|
||||||
if (!MODEL.auth) return
|
if (!MODEL.auth) return
|
||||||
throw e
|
throw e
|
||||||
@@ -216,10 +238,6 @@ export async function POST(input: APIEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function trackUsage(chunk: any) {
|
async function trackUsage(chunk: any) {
|
||||||
console.log(`trackUsage ${apiKey}`)
|
|
||||||
|
|
||||||
if (!apiKey) return
|
|
||||||
|
|
||||||
const usage = chunk.usage
|
const usage = chunk.usage
|
||||||
const inputTokens = usage.prompt_tokens ?? 0
|
const inputTokens = usage.prompt_tokens ?? 0
|
||||||
const outputTokens = usage.completion_tokens ?? 0
|
const outputTokens = usage.completion_tokens ?? 0
|
||||||
@@ -228,14 +246,30 @@ export async function POST(input: APIEvent) {
|
|||||||
//const cacheWriteTokens = providerMetadata?.["anthropic"]?.["cacheCreationInputTokens"] ?? 0
|
//const cacheWriteTokens = providerMetadata?.["anthropic"]?.["cacheCreationInputTokens"] ?? 0
|
||||||
const cacheWriteTokens = 0
|
const cacheWriteTokens = 0
|
||||||
|
|
||||||
const inputCost = MODEL.cost.input * inputTokens
|
const inputCost = MODEL.cost.input * inputTokens * 100
|
||||||
const outputCost = MODEL.cost.output * outputTokens
|
const outputCost = MODEL.cost.output * outputTokens * 100
|
||||||
const reasoningCost = MODEL.cost.reasoning * reasoningTokens
|
const reasoningCost = MODEL.cost.reasoning * reasoningTokens * 100
|
||||||
const cacheReadCost = MODEL.cost.cacheRead * cacheReadTokens
|
const cacheReadCost = MODEL.cost.cacheRead * cacheReadTokens * 100
|
||||||
const cacheWriteCost = MODEL.cost.cacheWrite * cacheWriteTokens
|
const cacheWriteCost = MODEL.cost.cacheWrite * cacheWriteTokens * 100
|
||||||
const costInCents = (inputCost + outputCost + reasoningCost + cacheReadCost + cacheWriteCost) * 100
|
const totalCostInCent = inputCost + outputCost + reasoningCost + cacheReadCost + cacheWriteCost
|
||||||
const cost = isFree ? 0 : centsToMicroCents(costInCents)
|
|
||||||
|
|
||||||
|
logMetric({
|
||||||
|
"tokens.input": inputTokens,
|
||||||
|
"tokens.output": outputTokens,
|
||||||
|
"tokens.reasoning": reasoningTokens,
|
||||||
|
"tokens.cache_read": cacheReadTokens,
|
||||||
|
"tokens.cache_write": cacheWriteTokens,
|
||||||
|
"cost.input": Math.round(inputCost),
|
||||||
|
"cost.output": Math.round(outputCost),
|
||||||
|
"cost.reasoning": Math.round(reasoningCost),
|
||||||
|
"cost.cache_read": Math.round(cacheReadCost),
|
||||||
|
"cost.cache_write": Math.round(cacheWriteCost),
|
||||||
|
"cost.total": Math.round(totalCostInCent),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!apiKey) return
|
||||||
|
|
||||||
|
const cost = isFree ? 0 : centsToMicroCents(totalCostInCent)
|
||||||
await Database.transaction(async (tx) => {
|
await Database.transaction(async (tx) => {
|
||||||
await tx.insert(UsageTable).values({
|
await tx.insert(UsageTable).values({
|
||||||
workspaceID: apiKey.workspaceID,
|
workspaceID: apiKey.workspaceID,
|
||||||
@@ -264,47 +298,18 @@ export async function POST(input: APIEvent) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error instanceof AuthError) {
|
logMetric({
|
||||||
return new Response(
|
"error.type": error.constructor.name,
|
||||||
JSON.stringify({
|
"error.message": error.message,
|
||||||
error: {
|
|
||||||
message: error.message,
|
|
||||||
type: "invalid_request_error",
|
|
||||||
param: null,
|
|
||||||
code: "unauthorized",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
status: 401,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error instanceof CreditsError) {
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
error: {
|
|
||||||
message: error.message,
|
|
||||||
type: "insufficient_quota",
|
|
||||||
param: null,
|
|
||||||
code: "insufficient_quota",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
status: 401,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error instanceof ModelError) {
|
|
||||||
return new Response(JSON.stringify({ error: { message: error.message } }), {
|
|
||||||
status: 401,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (error instanceof AuthError || error instanceof CreditsError || error instanceof ModelError)
|
||||||
|
return new Response(JSON.stringify({ error: { message: error.message } }), { status: 401 })
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({ error: { message: error.message } }), { status: 500 })
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(error)
|
function logMetric(values: Record<string, any>) {
|
||||||
return new Response(JSON.stringify({ error: { message: error.message } }), {
|
console.log(`_metric:${JSON.stringify(values)}`)
|
||||||
status: 500,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
49
cloud/function/src/log-processor.ts
Normal file
49
cloud/function/src/log-processor.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { Resource } from "@opencode/cloud-resource"
|
||||||
|
import type { TraceItem } from "@cloudflare/workers-types"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async tail(events: TraceItem[]) {
|
||||||
|
for (const event of events) {
|
||||||
|
if (!event.event) continue
|
||||||
|
if (!("request" in event.event)) continue
|
||||||
|
if (event.event.request.method !== "POST") continue
|
||||||
|
|
||||||
|
const url = new URL(event.event.request.url)
|
||||||
|
if (url.pathname !== "/zen/v1/chat/completions") return
|
||||||
|
|
||||||
|
let metrics = {
|
||||||
|
event_type: "completions",
|
||||||
|
"cf.continent": event.event.request.cf?.continent,
|
||||||
|
"cf.country": event.event.request.cf?.country,
|
||||||
|
"cf.city": event.event.request.cf?.city,
|
||||||
|
"cf.region": event.event.request.cf?.region,
|
||||||
|
"cf.latitude": event.event.request.cf?.latitude,
|
||||||
|
"cf.longitude": event.event.request.cf?.longitude,
|
||||||
|
"cf.timezone": event.event.request.cf?.timezone,
|
||||||
|
duration: event.wallTime,
|
||||||
|
request_length: parseInt(event.event.request.headers["content-length"] ?? "0"),
|
||||||
|
status: event.event.response?.status ?? 0,
|
||||||
|
ip: event.event.request.headers["x-real-ip"],
|
||||||
|
}
|
||||||
|
for (const log of event.logs) {
|
||||||
|
for (const message of log.message) {
|
||||||
|
if (!message.startsWith("_metric:")) continue
|
||||||
|
metrics = { ...metrics, ...JSON.parse(message.slice(8)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(JSON.stringify(metrics, null, 2))
|
||||||
|
|
||||||
|
const ret = await fetch("https://api.honeycomb.io/1/events/zen", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Honeycomb-Event-Time": (event.eventTimestamp ?? Date.now()).toString(),
|
||||||
|
"X-Honeycomb-Team": Resource.HONEYCOMB_API_KEY.value,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(metrics),
|
||||||
|
})
|
||||||
|
console.log(ret.status)
|
||||||
|
console.log(await ret.text())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
5
cloud/function/sst-env.d.ts
vendored
5
cloud/function/sst-env.d.ts
vendored
@@ -50,6 +50,10 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"HONEYCOMB_API_KEY": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
"STRIPE_SECRET_KEY": {
|
"STRIPE_SECRET_KEY": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
@@ -76,6 +80,7 @@ declare module "sst" {
|
|||||||
"AuthApi": cloudflare.Service
|
"AuthApi": cloudflare.Service
|
||||||
"AuthStorage": cloudflare.KVNamespace
|
"AuthStorage": cloudflare.KVNamespace
|
||||||
"Bucket": cloudflare.R2Bucket
|
"Bucket": cloudflare.R2Bucket
|
||||||
|
"LogProcessor": cloudflare.Service
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
5
cloud/resource/sst-env.d.ts
vendored
5
cloud/resource/sst-env.d.ts
vendored
@@ -50,6 +50,10 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"HONEYCOMB_API_KEY": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
"STRIPE_SECRET_KEY": {
|
"STRIPE_SECRET_KEY": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
@@ -76,6 +80,7 @@ declare module "sst" {
|
|||||||
"AuthApi": cloudflare.Service
|
"AuthApi": cloudflare.Service
|
||||||
"AuthStorage": cloudflare.KVNamespace
|
"AuthStorage": cloudflare.KVNamespace
|
||||||
"Bucket": cloudflare.R2Bucket
|
"Bucket": cloudflare.R2Bucket
|
||||||
|
"LogProcessor": cloudflare.Service
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { WebhookEndpoint } from "pulumi-stripe"
|
import { WebhookEndpoint } from "pulumi-stripe"
|
||||||
import { domain } from "./stage"
|
import { domain } from "./stage"
|
||||||
|
import log from "../packages/web/dist/_worker.js/chunks/log_GHQSQ8rj.mjs"
|
||||||
|
|
||||||
////////////////
|
////////////////
|
||||||
// DATABASE
|
// DATABASE
|
||||||
@@ -7,7 +8,7 @@ import { domain } from "./stage"
|
|||||||
|
|
||||||
const cluster = planetscale.getDatabaseOutput({
|
const cluster = planetscale.getDatabaseOutput({
|
||||||
name: "opencode",
|
name: "opencode",
|
||||||
organization: "sst",
|
organization: "anomalyco",
|
||||||
})
|
})
|
||||||
|
|
||||||
const branch =
|
const branch =
|
||||||
@@ -103,6 +104,7 @@ const ANTHROPIC_API_KEY = new sst.Secret("ANTHROPIC_API_KEY")
|
|||||||
const XAI_API_KEY = new sst.Secret("XAI_API_KEY")
|
const XAI_API_KEY = new sst.Secret("XAI_API_KEY")
|
||||||
const BASETEN_API_KEY = new sst.Secret("BASETEN_API_KEY")
|
const BASETEN_API_KEY = new sst.Secret("BASETEN_API_KEY")
|
||||||
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
||||||
|
const HONEYCOMB_API_KEY = new sst.Secret("HONEYCOMB_API_KEY")
|
||||||
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
|
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
|
||||||
properties: { value: auth.url.apply((url) => url!) },
|
properties: { value: auth.url.apply((url) => url!) },
|
||||||
})
|
})
|
||||||
@@ -114,6 +116,14 @@ const STRIPE_WEBHOOK_SECRET = new sst.Linkable("STRIPE_WEBHOOK_SECRET", {
|
|||||||
// CONSOLE
|
// CONSOLE
|
||||||
////////////////
|
////////////////
|
||||||
|
|
||||||
|
let logProcessor
|
||||||
|
if ($app.stage === "production" || $app.stage === "frank") {
|
||||||
|
logProcessor = new sst.cloudflare.Worker("LogProcessor", {
|
||||||
|
handler: "cloud/function/src/log-processor.ts",
|
||||||
|
link: [HONEYCOMB_API_KEY],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
new sst.cloudflare.x.SolidStart("Console", {
|
new sst.cloudflare.x.SolidStart("Console", {
|
||||||
domain,
|
domain,
|
||||||
path: "cloud/app",
|
path: "cloud/app",
|
||||||
@@ -135,9 +145,8 @@ new sst.cloudflare.x.SolidStart("Console", {
|
|||||||
server: {
|
server: {
|
||||||
transform: {
|
transform: {
|
||||||
worker: {
|
worker: {
|
||||||
placement: {
|
placement: { mode: "smart" },
|
||||||
mode: "smart",
|
tailConsumers: logProcessor ? [{ service: logProcessor.nodes.worker.scriptName }] : [],
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
5
packages/function/sst-env.d.ts
vendored
5
packages/function/sst-env.d.ts
vendored
@@ -50,6 +50,10 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"HONEYCOMB_API_KEY": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
"STRIPE_SECRET_KEY": {
|
"STRIPE_SECRET_KEY": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
@@ -76,6 +80,7 @@ declare module "sst" {
|
|||||||
"AuthApi": cloudflare.Service
|
"AuthApi": cloudflare.Service
|
||||||
"AuthStorage": cloudflare.KVNamespace
|
"AuthStorage": cloudflare.KVNamespace
|
||||||
"Bucket": cloudflare.R2Bucket
|
"Bucket": cloudflare.R2Bucket
|
||||||
|
"LogProcessor": cloudflare.Service
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
7
sst-env.d.ts
vendored
7
sst-env.d.ts
vendored
@@ -64,6 +64,13 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"HONEYCOMB_API_KEY": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"LogProcessor": {
|
||||||
|
"type": "sst.cloudflare.Worker"
|
||||||
|
}
|
||||||
"STRIPE_SECRET_KEY": {
|
"STRIPE_SECRET_KEY": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
|
|||||||
Reference in New Issue
Block a user