mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-12 02:58:34 +08:00
Compare commits
5 Commits
refactor/e
...
d2e5afb298
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2e5afb298 | ||
|
|
d3fb2314ee | ||
|
|
447bb30745 | ||
|
|
63398d9f34 | ||
|
|
82f4deb23a |
@@ -66,8 +66,22 @@ export const ModelSelectorInput = ({
|
|||||||
|
|
||||||
export type ModelSelectorListProps = ComponentProps<typeof CommandList>
|
export type ModelSelectorListProps = ComponentProps<typeof CommandList>
|
||||||
|
|
||||||
export const ModelSelectorList = (props: ModelSelectorListProps) => (
|
export const ModelSelectorList = ({
|
||||||
<CommandList {...props} />
|
className,
|
||||||
|
...props
|
||||||
|
}: ModelSelectorListProps) => (
|
||||||
|
<div className="relative">
|
||||||
|
<CommandList
|
||||||
|
className={cn(
|
||||||
|
// Hide scrollbar on all platforms
|
||||||
|
"[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{/* Bottom shadow indicator for scrollable content */}
|
||||||
|
<div className="pointer-events-none absolute bottom-0 left-0 right-0 h-12 bg-gradient-to-t from-muted/80 via-muted/40 to-transparent" />
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
export type ModelSelectorEmptyProps = ComponentProps<typeof CommandEmpty>
|
export type ModelSelectorEmptyProps = ComponentProps<typeof CommandEmpty>
|
||||||
|
|||||||
@@ -504,7 +504,7 @@ export function ModelConfigDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Provider Details (Right Panel) */}
|
{/* Provider Details (Right Panel) */}
|
||||||
<div className="flex-1 min-w-0 flex flex-col overflow-hidden">
|
<div className="flex-1 min-w-0 flex flex-col overflow-auto [&::-webkit-scrollbar]:hidden ">
|
||||||
{selectedProvider ? (
|
{selectedProvider ? (
|
||||||
<>
|
<>
|
||||||
<ScrollArea className="flex-1" ref={scrollRef}>
|
<ScrollArea className="flex-1" ref={scrollRef}>
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export function ModelSelector({
|
|||||||
<ModelSelectorInput
|
<ModelSelectorInput
|
||||||
placeholder={dict.modelConfig.searchModels}
|
placeholder={dict.modelConfig.searchModels}
|
||||||
/>
|
/>
|
||||||
<ModelSelectorList>
|
<ModelSelectorList className="[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
|
||||||
<ModelSelectorEmpty>
|
<ModelSelectorEmpty>
|
||||||
{validatedModels.length === 0 && models.length > 0
|
{validatedModels.length === 0 && models.length > 0
|
||||||
? dict.modelConfig.noVerifiedModels
|
? dict.modelConfig.noVerifiedModels
|
||||||
|
|||||||
@@ -14,22 +14,14 @@ export function register() {
|
|||||||
publicKey: process.env.LANGFUSE_PUBLIC_KEY,
|
publicKey: process.env.LANGFUSE_PUBLIC_KEY,
|
||||||
secretKey: process.env.LANGFUSE_SECRET_KEY,
|
secretKey: process.env.LANGFUSE_SECRET_KEY,
|
||||||
baseUrl: process.env.LANGFUSE_BASEURL,
|
baseUrl: process.env.LANGFUSE_BASEURL,
|
||||||
// Filter out Next.js HTTP request spans so AI SDK spans become root traces
|
// Whitelist approach: only export AI-related spans
|
||||||
shouldExportSpan: ({ otelSpan }) => {
|
shouldExportSpan: ({ otelSpan }) => {
|
||||||
const spanName = otelSpan.name
|
const spanName = otelSpan.name
|
||||||
// Skip Next.js HTTP infrastructure spans
|
// Only export AI SDK spans (ai.*) and our explicit "chat" wrapper
|
||||||
if (
|
if (spanName === "chat" || spanName.startsWith("ai.")) {
|
||||||
spanName.startsWith("POST") ||
|
|
||||||
spanName.startsWith("GET") ||
|
|
||||||
spanName.startsWith("RSC") ||
|
|
||||||
spanName.includes("BaseServer") ||
|
|
||||||
spanName.includes("handleRequest") ||
|
|
||||||
spanName.includes("resolve page") ||
|
|
||||||
spanName.includes("start response")
|
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,30 @@ import {
|
|||||||
// OSS users who don't need quota tracking can simply not set this env var
|
// OSS users who don't need quota tracking can simply not set this env var
|
||||||
const TABLE = process.env.DYNAMODB_QUOTA_TABLE
|
const TABLE = process.env.DYNAMODB_QUOTA_TABLE
|
||||||
const DYNAMODB_REGION = process.env.DYNAMODB_REGION || "ap-northeast-1"
|
const DYNAMODB_REGION = process.env.DYNAMODB_REGION || "ap-northeast-1"
|
||||||
|
// Timezone for daily quota reset (e.g., "Asia/Tokyo" for JST midnight reset)
|
||||||
|
// Defaults to UTC if not set
|
||||||
|
let QUOTA_TIMEZONE = process.env.QUOTA_TIMEZONE || "UTC"
|
||||||
|
|
||||||
|
// Validate timezone at module load
|
||||||
|
try {
|
||||||
|
new Intl.DateTimeFormat("en-CA", { timeZone: QUOTA_TIMEZONE }).format(
|
||||||
|
new Date(),
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
console.warn(
|
||||||
|
`[quota] Invalid QUOTA_TIMEZONE "${QUOTA_TIMEZONE}", using UTC`,
|
||||||
|
)
|
||||||
|
QUOTA_TIMEZONE = "UTC"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get today's date string in the configured timezone (YYYY-MM-DD format)
|
||||||
|
*/
|
||||||
|
function getTodayInTimezone(): string {
|
||||||
|
return new Intl.DateTimeFormat("en-CA", {
|
||||||
|
timeZone: QUOTA_TIMEZONE,
|
||||||
|
}).format(new Date())
|
||||||
|
}
|
||||||
|
|
||||||
// Only create client if quota is enabled
|
// Only create client if quota is enabled
|
||||||
const client = TABLE ? new DynamoDBClient({ region: DYNAMODB_REGION }) : null
|
const client = TABLE ? new DynamoDBClient({ region: DYNAMODB_REGION }) : null
|
||||||
@@ -49,32 +73,67 @@ export async function checkAndIncrementRequest(
|
|||||||
return { allowed: true }
|
return { allowed: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
const today = new Date().toISOString().split("T")[0]
|
const today = getTodayInTimezone()
|
||||||
const currentMinute = Math.floor(Date.now() / 60000).toString()
|
const currentMinute = Math.floor(Date.now() / 60000).toString()
|
||||||
const ttl = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
|
const ttl = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Atomic check-and-increment with ConditionExpression
|
// First, try to reset counts if it's a new day (atomic day reset)
|
||||||
// This prevents race conditions by failing if limits are exceeded
|
// This will succeed only if lastResetDate < today or doesn't exist
|
||||||
|
try {
|
||||||
await client.send(
|
await client.send(
|
||||||
new UpdateItemCommand({
|
new UpdateItemCommand({
|
||||||
TableName: TABLE,
|
TableName: TABLE,
|
||||||
Key: { PK: { S: `IP#${ip}` } },
|
Key: { PK: { S: `IP#${ip}` } },
|
||||||
// Reset counts if new day/minute, then increment request count
|
// Reset all counts to 1/0 for the new day
|
||||||
UpdateExpression: `
|
UpdateExpression: `
|
||||||
SET lastResetDate = :today,
|
SET lastResetDate = :today,
|
||||||
dailyReqCount = if_not_exists(dailyReqCount, :zero) + :one,
|
dailyReqCount = :one,
|
||||||
dailyTokenCount = if_not_exists(dailyTokenCount, :zero),
|
dailyTokenCount = :zero,
|
||||||
lastMinute = :minute,
|
lastMinute = :minute,
|
||||||
tpmCount = if_not_exists(tpmCount, :zero),
|
tpmCount = :zero,
|
||||||
#ttl = :ttl
|
#ttl = :ttl
|
||||||
`,
|
`,
|
||||||
// Atomic condition: only succeed if ALL limits pass
|
// Only succeed if it's a new day (or new item)
|
||||||
// Uses attribute_not_exists for new items, then checks limits for existing items
|
|
||||||
ConditionExpression: `
|
ConditionExpression: `
|
||||||
(attribute_not_exists(lastResetDate) OR lastResetDate < :today OR
|
attribute_not_exists(lastResetDate) OR lastResetDate < :today
|
||||||
((attribute_not_exists(dailyReqCount) OR dailyReqCount < :reqLimit) AND
|
`,
|
||||||
(attribute_not_exists(dailyTokenCount) OR dailyTokenCount < :tokenLimit))) AND
|
ExpressionAttributeNames: { "#ttl": "ttl" },
|
||||||
|
ExpressionAttributeValues: {
|
||||||
|
":today": { S: today },
|
||||||
|
":zero": { N: "0" },
|
||||||
|
":one": { N: "1" },
|
||||||
|
":minute": { S: currentMinute },
|
||||||
|
":ttl": { N: String(ttl) },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
// New day reset successful
|
||||||
|
return { allowed: true }
|
||||||
|
} catch (resetError: any) {
|
||||||
|
// If condition failed, it's the same day - continue to increment logic
|
||||||
|
if (!(resetError instanceof ConditionalCheckFailedException)) {
|
||||||
|
throw resetError // Re-throw unexpected errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same day - increment request count with limit checks
|
||||||
|
await client.send(
|
||||||
|
new UpdateItemCommand({
|
||||||
|
TableName: TABLE,
|
||||||
|
Key: { PK: { S: `IP#${ip}` } },
|
||||||
|
// Increment request count, handle minute boundary for TPM
|
||||||
|
UpdateExpression: `
|
||||||
|
SET lastMinute = :minute,
|
||||||
|
tpmCount = if_not_exists(tpmCount, :zero),
|
||||||
|
#ttl = :ttl
|
||||||
|
ADD dailyReqCount :one
|
||||||
|
`,
|
||||||
|
// Check all limits before allowing increment
|
||||||
|
ConditionExpression: `
|
||||||
|
lastResetDate = :today AND
|
||||||
|
(attribute_not_exists(dailyReqCount) OR dailyReqCount < :reqLimit) AND
|
||||||
|
(attribute_not_exists(dailyTokenCount) OR dailyTokenCount < :tokenLimit) AND
|
||||||
(attribute_not_exists(lastMinute) OR lastMinute <> :minute OR
|
(attribute_not_exists(lastMinute) OR lastMinute <> :minute OR
|
||||||
attribute_not_exists(tpmCount) OR tpmCount < :tpmLimit)
|
attribute_not_exists(tpmCount) OR tpmCount < :tpmLimit)
|
||||||
`,
|
`,
|
||||||
|
|||||||
Reference in New Issue
Block a user