Compare commits

...

3 Commits

Author SHA1 Message Date
dayuan.jiang
0dd7b2383e WIP: Cloudflare Worker deployment setup 2025-12-08 10:31:10 +09:00
dayuan.jiang
167f5ed36a feat: enable recordInputs in Langfuse telemetry
Enable full message history recording including XML tool calls for better observability.
2025-12-07 20:58:44 +09:00
Dayuan Jiang
cd8e0e2263 feat: add token counting utility for system prompts (#153)
Co-authored-by: dayuan.jiang <jiangdy@amazon.co.jp>
2025-12-07 20:33:43 +09:00
12 changed files with 8482 additions and 94 deletions

7
.gitignore vendored
View File

@@ -41,4 +41,9 @@ yarn-error.log*
next-env.d.ts
push-via-ec2.sh
.claude/settings.local.json
.playwright-mcp/
.playwright-mcp/
# cloudflare
.open-next/
.dev.vars
.wrangler/

3
cloudflare-env.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
interface CloudflareEnv {
ASSETS: Fetcher
}

View File

@@ -1,7 +1,15 @@
import { LangfuseSpanProcessor } from "@langfuse/otel"
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node"
export function register() {
// Skip on edge/worker runtime (Cloudflare Workers, Vercel Edge)
// OpenTelemetry Node SDK requires Node.js-specific APIs
if (
typeof process === "undefined" ||
!process.versions?.node ||
// @ts-expect-error - EdgeRuntime is a global in edge environments
typeof EdgeRuntime !== "undefined"
) {
return
}
// Skip telemetry if Langfuse env vars are not configured
if (!process.env.LANGFUSE_PUBLIC_KEY || !process.env.LANGFUSE_SECRET_KEY) {
console.warn(
@@ -10,12 +18,16 @@ export function register() {
return
}
// Dynamic imports to avoid bundling Node.js-specific modules in edge builds
const { LangfuseSpanProcessor } = require("@langfuse/otel")
const { NodeTracerProvider } = require("@opentelemetry/sdk-trace-node")
const langfuseSpanProcessor = new LangfuseSpanProcessor({
publicKey: process.env.LANGFUSE_PUBLIC_KEY,
secretKey: process.env.LANGFUSE_SECRET_KEY,
baseUrl: process.env.LANGFUSE_BASEURL,
// Filter out Next.js HTTP request spans so AI SDK spans become root traces
shouldExportSpan: ({ otelSpan }) => {
shouldExportSpan: ({ otelSpan }: { otelSpan: { name: string } }) => {
const spanName = otelSpan.name
// Skip Next.js HTTP infrastructure spans
if (

View File

@@ -4,10 +4,16 @@ import { azure, createAzure } from "@ai-sdk/azure"
import { createDeepSeek, deepseek } from "@ai-sdk/deepseek"
import { createGoogleGenerativeAI, google } from "@ai-sdk/google"
import { createOpenAI, openai } from "@ai-sdk/openai"
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
import { createOpenRouter } from "@openrouter/ai-sdk-provider"
import { createOllama, ollama } from "ollama-ai-provider-v2"
// Detect if running in edge/worker runtime (Cloudflare Workers, Vercel Edge, etc.)
const isEdgeRuntime =
typeof process === "undefined" ||
!process.versions?.node ||
// @ts-expect-error - EdgeRuntime is a global in edge environments
typeof EdgeRuntime !== "undefined"
export type ProviderName =
| "bedrock"
| "openai"
@@ -166,12 +172,37 @@ export function getAIModel(): ModelConfig {
switch (provider) {
case "bedrock": {
// Use credential provider chain for IAM role support (Amplify, Lambda, etc.)
// Falls back to env vars (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) for local dev
const bedrockProvider = createAmazonBedrock({
// Edge runtime (Cloudflare Workers, etc.) requires explicit credentials
// Node.js runtime can use IAM role chain (Amplify, Lambda, etc.)
const bedrockConfig: Parameters<typeof createAmazonBedrock>[0] = {
region: process.env.AWS_REGION || "us-west-2",
credentialProvider: fromNodeProviderChain(),
})
}
if (isEdgeRuntime) {
// Edge runtime: use explicit credentials from env vars
if (
!process.env.AWS_ACCESS_KEY_ID ||
!process.env.AWS_SECRET_ACCESS_KEY
) {
throw new Error(
"AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are required for Bedrock on edge runtime (Cloudflare Workers)",
)
}
bedrockConfig.accessKeyId = process.env.AWS_ACCESS_KEY_ID
bedrockConfig.secretAccessKey =
process.env.AWS_SECRET_ACCESS_KEY
if (process.env.AWS_SESSION_TOKEN) {
bedrockConfig.sessionToken = process.env.AWS_SESSION_TOKEN
}
} else {
// Node.js runtime: use credential provider chain for IAM role support
const {
fromNodeProviderChain,
} = require("@aws-sdk/credential-providers")
bedrockConfig.credentialProvider = fromNodeProviderChain()
}
const bedrockProvider = createAmazonBedrock(bedrockConfig)
model = bedrockProvider(modelId)
// Add Anthropic beta options if using Claude models via Bedrock
if (modelId.includes("anthropic.claude")) {

View File

@@ -84,9 +84,7 @@ export function getTelemetryConfig(params: {
return {
isEnabled: true,
// Disable automatic input recording to avoid uploading large base64 images to Langfuse media
// User text input is recorded manually via setTraceInput
recordInputs: false,
recordInputs: true,
recordOutputs: true,
metadata: {
sessionId: params.sessionId,

View File

@@ -1,9 +1,12 @@
/**
* System prompts for different AI models
* Extended prompt is used for models with higher cache token minimums (Opus 4.5, Haiku 4.5)
*
* Token counting utilities are in a separate file (token-counter.ts) to avoid
* WebAssembly issues with Next.js server-side rendering.
*/
// Default system prompt (~2700 tokens) - works with all models
// Default system prompt (~1900 tokens) - works with all models
export const DEFAULT_SYSTEM_PROMPT = `
You are an expert diagram creation assistant specializing in draw.io XML generation.
Your primary function is chat with user and crafting clear, well-organized visual diagrams through precise XML specifications.
@@ -132,8 +135,8 @@ Common styles:
`
// Extended additions (~1800 tokens) - appended for models with 4000 token cache minimum
// Total EXTENDED_SYSTEM_PROMPT = ~4500 tokens
// Extended additions (~2600 tokens) - appended for models with 4000 token cache minimum
// Total EXTENDED_SYSTEM_PROMPT = ~4400 tokens
const EXTENDED_ADDITIONS = `
## Extended Tool Reference

38
lib/token-counter.ts Normal file
View File

@@ -0,0 +1,38 @@
/**
* Token counting utilities using Anthropic's tokenizer
*
* This file is separate from system-prompts.ts because the @anthropic-ai/tokenizer
* package uses WebAssembly which doesn't work well with Next.js server-side rendering.
* Import this file only in scripts or client-side code, not in API routes.
*/
import { countTokens } from "@anthropic-ai/tokenizer"
import { DEFAULT_SYSTEM_PROMPT, EXTENDED_SYSTEM_PROMPT } from "./system-prompts"
/**
* Count the number of tokens in a text string using Anthropic's tokenizer
* @param text - The text to count tokens for
* @returns The number of tokens
*/
export function countTextTokens(text: string): number {
return countTokens(text)
}
/**
* Get token counts for the system prompts
* Useful for debugging and optimizing prompt sizes
* @returns Object with token counts for default and extended prompts
*/
export function getSystemPromptTokenCounts(): {
default: number
extended: number
additions: number
} {
const defaultTokens = countTokens(DEFAULT_SYSTEM_PROMPT)
const extendedTokens = countTokens(EXTENDED_SYSTEM_PROMPT)
return {
default: defaultTokens,
extended: extendedTokens,
additions: extendedTokens - defaultTokens,
}
}

3
open-next.config.ts Normal file
View File

@@ -0,0 +1,3 @@
import { defineCloudflareConfig } from "@opennextjs/cloudflare"
export default defineCloudflareConfig()

8429
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,11 @@
"lint": "biome lint .",
"format": "biome check --write .",
"check": "biome ci",
"prepare": "husky"
"prepare": "husky",
"cf:build": "opennextjs-cloudflare build",
"cf:preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
"cf:deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
"cf:typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
},
"dependencies": {
"@ai-sdk/amazon-bedrock": "^3.0.62",
@@ -25,6 +29,7 @@
"@langfuse/otel": "^4.4.4",
"@langfuse/tracing": "^4.4.9",
"@next/third-parties": "^16.0.6",
"@opennextjs/cloudflare": "^1.14.4",
"@openrouter/ai-sdk-provider": "^1.2.3",
"@opentelemetry/exporter-trace-otlp-http": "^0.208.0",
"@opentelemetry/sdk-trace-node": "^2.2.0",
@@ -66,6 +71,7 @@
]
},
"devDependencies": {
"@anthropic-ai/tokenizer": "^0.0.4",
"@biomejs/biome": "2.3.8",
"@tailwindcss/postcss": "^4",
"@tailwindcss/typography": "^0.5.19",
@@ -78,6 +84,7 @@
"husky": "^9.1.7",
"lint-staged": "^16.2.7",
"tailwindcss": "^4",
"typescript": "^5"
"typescript": "^5",
"wrangler": "^4.53.0"
}
}

View File

@@ -24,6 +24,7 @@
},
"include": [
"next-env.d.ts",
"cloudflare-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",

8
wrangler.toml Normal file
View File

@@ -0,0 +1,8 @@
main = ".open-next/worker.js"
name = "next-ai-draw-io"
compatibility_date = "2024-09-23"
compatibility_flags = ["nodejs_compat"]
[assets]
directory = ".open-next/assets"
binding = "ASSETS"