feat: initial datalake and stats site (#28666)
This commit is contained in:
@@ -6,7 +6,7 @@ export const logger = {
|
||||
},
|
||||
log: console.log,
|
||||
debug: (message: string) => {
|
||||
if (Resource.App.stage === "production") return
|
||||
if (Resource.App.stage === "production" || Resource.App.stage === "adam") return
|
||||
console.debug(message)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export default defineConfig({
|
||||
}) as PluginOption,
|
||||
nitro({
|
||||
compatibilityDate: "2024-09-19",
|
||||
preset: "cloudflare_module",
|
||||
preset: "cloudflare-module",
|
||||
cloudflare: {
|
||||
nodeCompat: true,
|
||||
},
|
||||
|
||||
146
packages/console/core/script/create-api-key.ts
Normal file
146
packages/console/core/script/create-api-key.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import { and, Database, eq, isNull } from "../src/drizzle/index.js"
|
||||
import { Identifier } from "../src/identifier.js"
|
||||
import { AccountTable } from "../src/schema/account.sql.js"
|
||||
import { AuthTable } from "../src/schema/auth.sql.js"
|
||||
import { BillingTable } from "../src/schema/billing.sql.js"
|
||||
import { KeyTable } from "../src/schema/key.sql.js"
|
||||
import { UserTable } from "../src/schema/user.sql.js"
|
||||
import { WorkspaceTable } from "../src/schema/workspace.sql.js"
|
||||
import { centsToMicroCents } from "../src/util/price.js"
|
||||
|
||||
const args = parseArgs(process.argv.slice(2))
|
||||
if (!args.email) {
|
||||
console.error(
|
||||
"Usage: bun script/create-api-key.ts --email <email> [--workspace-id <wrk_...>] [--workspace-name <name>] [--key-name <name>] [--balance-dollars <amount>] [--allow-production]",
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
if (Resource.App.stage === "production" && !args.allowProduction) {
|
||||
throw new Error("Refusing to create a production API key without --allow-production")
|
||||
}
|
||||
|
||||
const result = await Database.transaction(async (tx) => {
|
||||
const auth = await tx
|
||||
.select()
|
||||
.from(AuthTable)
|
||||
.where(and(eq(AuthTable.provider, "email"), eq(AuthTable.subject, args.email)))
|
||||
.then((rows) => rows[0])
|
||||
const accountID = auth?.accountID ?? Identifier.create("account")
|
||||
if (!auth) {
|
||||
await tx.insert(AccountTable).values({ id: accountID })
|
||||
await tx.insert(AuthTable).values({
|
||||
id: Identifier.create("auth"),
|
||||
provider: "email",
|
||||
subject: args.email,
|
||||
accountID,
|
||||
})
|
||||
}
|
||||
|
||||
const workspace = args.workspaceID
|
||||
? await tx
|
||||
.select()
|
||||
.from(WorkspaceTable)
|
||||
.where(eq(WorkspaceTable.id, args.workspaceID))
|
||||
.then((rows) => rows[0])
|
||||
: await tx
|
||||
.select({ workspace: WorkspaceTable })
|
||||
.from(UserTable)
|
||||
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, UserTable.workspaceID))
|
||||
.where(and(eq(UserTable.accountID, accountID), isNull(UserTable.timeDeleted)))
|
||||
.then((rows) => rows[0]?.workspace)
|
||||
if (args.workspaceID && !workspace) throw new Error(`Workspace not found: ${args.workspaceID}`)
|
||||
const workspaceID = workspace?.id ?? Identifier.create("workspace")
|
||||
if (!workspace) {
|
||||
await tx.insert(WorkspaceTable).values({
|
||||
id: workspaceID,
|
||||
slug: null,
|
||||
name: args.workspaceName ?? `${args.email} manual`,
|
||||
})
|
||||
}
|
||||
|
||||
const user = await tx
|
||||
.select()
|
||||
.from(UserTable)
|
||||
.where(
|
||||
and(eq(UserTable.workspaceID, workspaceID), eq(UserTable.accountID, accountID), isNull(UserTable.timeDeleted)),
|
||||
)
|
||||
.then((rows) => rows[0])
|
||||
const userID = user?.id ?? Identifier.create("user")
|
||||
if (!user) {
|
||||
await tx.insert(UserTable).values({
|
||||
id: userID,
|
||||
workspaceID,
|
||||
accountID,
|
||||
email: args.email,
|
||||
name: args.email,
|
||||
role: "admin",
|
||||
})
|
||||
}
|
||||
|
||||
const balance = centsToMicroCents(args.balanceDollars * 100)
|
||||
const billing = await tx
|
||||
.select()
|
||||
.from(BillingTable)
|
||||
.where(eq(BillingTable.workspaceID, workspaceID))
|
||||
.then((rows) => rows[0])
|
||||
if (!billing) {
|
||||
await tx.insert(BillingTable).values({
|
||||
id: Identifier.create("billing"),
|
||||
workspaceID,
|
||||
balance,
|
||||
})
|
||||
} else if (billing.balance < balance) {
|
||||
await tx.update(BillingTable).set({ balance }).where(eq(BillingTable.workspaceID, workspaceID))
|
||||
}
|
||||
|
||||
const secretKey = createSecretKey()
|
||||
const keyID = Identifier.create("key")
|
||||
await tx.insert(KeyTable).values({
|
||||
id: keyID,
|
||||
workspaceID,
|
||||
userID,
|
||||
name: args.keyName ?? "Manual API Key",
|
||||
key: secretKey,
|
||||
timeUsed: null,
|
||||
})
|
||||
|
||||
return { accountID, workspaceID, userID, keyID, secretKey }
|
||||
})
|
||||
|
||||
console.log(JSON.stringify({ stage: Resource.App.stage, ...result }, null, 2))
|
||||
|
||||
function createSecretKey() {
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
const values = new Uint32Array(64)
|
||||
crypto.getRandomValues(values)
|
||||
return `sk-${Array.from(values, (value) => chars[value % chars.length]).join("")}`
|
||||
}
|
||||
|
||||
function parseArgs(argv: string[]) {
|
||||
const parsed = {
|
||||
email: "",
|
||||
workspaceID: "",
|
||||
workspaceName: "",
|
||||
keyName: "",
|
||||
balanceDollars: 100,
|
||||
allowProduction: false,
|
||||
}
|
||||
for (let index = 0; index < argv.length; index++) {
|
||||
const arg = argv[index]
|
||||
if (arg === "--email") parsed.email = requiredValue(argv, ++index, arg)
|
||||
if (arg === "--workspace-id") parsed.workspaceID = requiredValue(argv, ++index, arg)
|
||||
if (arg === "--workspace-name") parsed.workspaceName = requiredValue(argv, ++index, arg)
|
||||
if (arg === "--key-name") parsed.keyName = requiredValue(argv, ++index, arg)
|
||||
if (arg === "--balance-dollars") parsed.balanceDollars = Number(requiredValue(argv, ++index, arg))
|
||||
if (arg === "--allow-production") parsed.allowProduction = true
|
||||
}
|
||||
if (!Number.isFinite(parsed.balanceDollars) || parsed.balanceDollars < 0) throw new Error("Invalid --balance-dollars")
|
||||
return parsed
|
||||
}
|
||||
|
||||
function requiredValue(argv: string[], index: number, arg: string) {
|
||||
const value = argv[index]
|
||||
if (!value || value.startsWith("--")) throw new Error(`Missing value for ${arg}`)
|
||||
return value
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export default {
|
||||
)
|
||||
continue
|
||||
|
||||
let data = {
|
||||
let data: Record<string, unknown> = {
|
||||
"cf.continent": event.event.request.cf?.continent,
|
||||
"cf.country": event.event.request.cf?.country,
|
||||
"cf.city": event.event.request.cf?.city,
|
||||
@@ -35,30 +35,152 @@ export default {
|
||||
ip: event.event.request.headers["x-real-ip"],
|
||||
}
|
||||
const time = new Date(event.eventTimestamp ?? Date.now()).toISOString()
|
||||
const events = []
|
||||
for (const log of event.logs) {
|
||||
for (const message of log.message) {
|
||||
if (!message.startsWith("_metric:")) continue
|
||||
const json = JSON.parse(message.slice(8))
|
||||
data = { ...data, ...json }
|
||||
if ("llm.error.code" in json) {
|
||||
events.push({ time, data: { ...data, event_type: "llm.error" } })
|
||||
}
|
||||
}
|
||||
}
|
||||
events.push({ time, data: { ...data, event_type: "completions" } })
|
||||
const events = [
|
||||
...event.logs.flatMap((log) =>
|
||||
log.message.flatMap((message: string) => {
|
||||
if (!message.startsWith("_metric:")) return []
|
||||
const json = JSON.parse(message.slice(8)) as Record<string, unknown>
|
||||
data = { ...data, ...json }
|
||||
if ("llm.error.code" in json) {
|
||||
return [{ time, data: { ...data, event_type: "llm.error" } }]
|
||||
}
|
||||
return []
|
||||
}),
|
||||
),
|
||||
{ time, data: { ...data, event_type: "completions" } },
|
||||
]
|
||||
console.log(JSON.stringify(data, null, 2))
|
||||
|
||||
const ret = await fetch("https://api.honeycomb.io/1/batch/zen", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Honeycomb-Team": Resource.HONEYCOMB_API_KEY.value,
|
||||
},
|
||||
body: JSON.stringify(events),
|
||||
})
|
||||
console.log(ret.status)
|
||||
console.log(await ret.text())
|
||||
const lakeIngest = getLakeIngest()
|
||||
const [honeycomb, lake] = await Promise.all([
|
||||
fetch("https://api.honeycomb.io/1/batch/zen", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Honeycomb-Team": Resource.HONEYCOMB_API_KEY.value,
|
||||
},
|
||||
body: JSON.stringify(events),
|
||||
}),
|
||||
...(lakeIngest
|
||||
? [
|
||||
fetch(lakeIngest.url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${lakeIngest.secret}`,
|
||||
},
|
||||
body: JSON.stringify({ events: events.map((event) => toLakeEvent(event.time, event.data)) }),
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
])
|
||||
console.log(honeycomb.status)
|
||||
console.log(await honeycomb.text())
|
||||
if (lake) {
|
||||
console.log(lake.status)
|
||||
console.log(await lake.text())
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
function getLakeIngest(): { url: string; secret: string } | undefined {
|
||||
try {
|
||||
return Resource.LakeIngest
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
function toLakeEvent(time: string, data: Record<string, unknown>) {
|
||||
return {
|
||||
_datalake_key: "inference.event",
|
||||
event_timestamp: time,
|
||||
event_date: time.slice(0, 10),
|
||||
event_type: string(data, "event_type"),
|
||||
dataset: "zen",
|
||||
cf_continent: string(data, "cf.continent"),
|
||||
cf_country: string(data, "cf.country"),
|
||||
cf_city: string(data, "cf.city"),
|
||||
cf_region: string(data, "cf.region"),
|
||||
cf_latitude: number(data, "cf.latitude"),
|
||||
cf_longitude: number(data, "cf.longitude"),
|
||||
cf_timezone: string(data, "cf.timezone"),
|
||||
duration: number(data, "duration"),
|
||||
request_length: integer(data, "request_length"),
|
||||
status: integer(data, "status"),
|
||||
ip: string(data, "ip"),
|
||||
is_stream: boolean(data, "is_stream"),
|
||||
session: string(data, "session"),
|
||||
request: string(data, "request"),
|
||||
client: string(data, "client"),
|
||||
user_agent: string(data, "user_agent"),
|
||||
model_variant: string(data, "model.variant"),
|
||||
source: string(data, "source"),
|
||||
provider: string(data, "provider"),
|
||||
provider_model: string(data, "provider.model"),
|
||||
model: string(data, "model"),
|
||||
llm_error_code: integer(data, "llm.error.code"),
|
||||
llm_error_message: string(data, "llm.error.message"),
|
||||
error_response: string(data, "error.response"),
|
||||
error_type: string(data, "error.type"),
|
||||
error_message: string(data, "error.message"),
|
||||
error_cause: string(data, "error.cause"),
|
||||
error_cause2: string(data, "error.cause2"),
|
||||
api_key: string(data, "api_key"),
|
||||
workspace: string(data, "workspace"),
|
||||
is_subscription: boolean(data, "isSubscription"),
|
||||
subscription: string(data, "subscription"),
|
||||
response_length: integer(data, "response_length"),
|
||||
time_to_first_byte: integer(data, "time_to_first_byte"),
|
||||
timestamp_first_byte: integer(data, "timestamp.first_byte"),
|
||||
timestamp_last_byte: integer(data, "timestamp.last_byte"),
|
||||
tokens_input: integer(data, "tokens.input"),
|
||||
tokens_output: integer(data, "tokens.output"),
|
||||
tokens_reasoning: integer(data, "tokens.reasoning"),
|
||||
tokens_cache_read: integer(data, "tokens.cache_read"),
|
||||
tokens_cache_write_5m: integer(data, "tokens.cache_write_5m"),
|
||||
tokens_cache_write_1h: integer(data, "tokens.cache_write_1h"),
|
||||
cost_input_microcents: integer(data, "cost.input.microcents"),
|
||||
cost_output_microcents: integer(data, "cost.output.microcents"),
|
||||
cost_cache_read_microcents: integer(data, "cost.cache_read.microcents"),
|
||||
cost_cache_write_microcents: integer(data, "cost.cache_write.microcents"),
|
||||
cost_total_microcents: integer(data, "cost.total.microcents"),
|
||||
cost_input: integer(data, "cost.input"),
|
||||
cost_output: integer(data, "cost.output"),
|
||||
cost_cache_read: integer(data, "cost.cache_read"),
|
||||
cost_cache_write_5m: integer(data, "cost.cache_write_5m"),
|
||||
cost_cache_write_1h: integer(data, "cost.cache_write_1h"),
|
||||
cost_total: integer(data, "cost.total"),
|
||||
}
|
||||
}
|
||||
|
||||
function string(data: Record<string, unknown>, key: string) {
|
||||
const value = data[key]
|
||||
if (typeof value === "string") return value
|
||||
if (typeof value === "number" || typeof value === "boolean") return String(value)
|
||||
return undefined
|
||||
}
|
||||
|
||||
function boolean(data: Record<string, unknown>, key: string) {
|
||||
const value = data[key]
|
||||
if (typeof value === "boolean") return value
|
||||
if (typeof value === "string") return value === "true" ? true : value === "false" ? false : undefined
|
||||
return undefined
|
||||
}
|
||||
|
||||
function integer(data: Record<string, unknown>, key: string) {
|
||||
const value = number(data, key)
|
||||
if (value === undefined) return undefined
|
||||
return Math.round(value)
|
||||
}
|
||||
|
||||
function number(data: Record<string, unknown>, key: string) {
|
||||
const value = data[key]
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value : undefined
|
||||
if (typeof value === "string") {
|
||||
const parsed = Number(value)
|
||||
return Number.isFinite(parsed) ? parsed : undefined
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export const Resource = new Proxy(
|
||||
{
|
||||
get(_target, prop: keyof typeof ResourceBase) {
|
||||
const value = ResourceBase[prop]
|
||||
const secrets = ResourceBase as unknown as Record<string, { value: string }>
|
||||
if ("type" in value) {
|
||||
// @ts-ignore
|
||||
if (value.type === "sst.cloudflare.Bucket") {
|
||||
@@ -21,11 +22,11 @@ export const Resource = new Proxy(
|
||||
// @ts-ignore
|
||||
if (value.type === "sst.cloudflare.Kv") {
|
||||
const client = new Cloudflare({
|
||||
apiToken: ResourceBase.CLOUDFLARE_API_TOKEN.value,
|
||||
apiToken: secrets.CLOUDFLARE_API_TOKEN.value,
|
||||
})
|
||||
// @ts-ignore
|
||||
const namespaceId = value.namespaceId
|
||||
const accountId = ResourceBase.CLOUDFLARE_DEFAULT_ACCOUNT_ID.value
|
||||
const accountId = secrets.CLOUDFLARE_DEFAULT_ACCOUNT_ID.value
|
||||
return {
|
||||
get: (k: string | string[]) => {
|
||||
const isMulti = Array.isArray(k)
|
||||
|
||||
Reference in New Issue
Block a user