-
Notifications
You must be signed in to change notification settings - Fork 705
Split the deploy tool call into several steps so we can get better titles #293
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,115 +1,121 @@ | ||
| import { ConvexError, v } from "convex/values"; | ||
| import { httpAction, internalMutation, mutation } from "./_generated/server"; | ||
| import { getCurrentMember } from "./sessions"; | ||
| import { internal } from "./_generated/api"; | ||
| import { ConvexError, v } from 'convex/values'; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (ignore this -- we don't have linters on the |
||
| import { httpAction, internalMutation, mutation } from './_generated/server'; | ||
| import { getCurrentMember } from './sessions'; | ||
| import { internal } from './_generated/api'; | ||
|
|
||
| export const openaiProxy = httpAction(async (ctx, req) => { | ||
| if (!openaiProxyEnabled()) { | ||
| return new Response("Convex OpenAI proxy is disabled.", { status: 400 }); | ||
| } | ||
| if (!process.env.OPENAI_API_KEY) { | ||
| throw new Error("OPENAI_API_KEY is not set"); | ||
| } | ||
| const headers = new Headers(req.headers); | ||
| const authHeader = headers.get("Authorization"); | ||
| if (!authHeader) { | ||
| return new Response("Unauthorized", { status: 401 }); | ||
| } | ||
| if (!authHeader.startsWith("Bearer ")) { | ||
| return new Response("Invalid authorization header", { status: 401 }); | ||
| } | ||
| const token = authHeader.slice(7); | ||
| const result = await ctx.runMutation(internal.openaiProxy.decrementToken, { token }); | ||
| if (!result.success) { | ||
| return new Response(result.error, { status: 401 }); | ||
| } | ||
| if (!openaiProxyEnabled()) { | ||
| return new Response('Convex OpenAI proxy is disabled.', { status: 400 }); | ||
| } | ||
| if (!process.env.OPENAI_API_KEY) { | ||
| throw new Error('OPENAI_API_KEY is not set'); | ||
| } | ||
| const headers = new Headers(req.headers); | ||
| const authHeader = headers.get('Authorization'); | ||
| if (!authHeader) { | ||
| return new Response('Unauthorized', { status: 401 }); | ||
| } | ||
| if (!authHeader.startsWith('Bearer ')) { | ||
| return new Response('Invalid authorization header', { status: 401 }); | ||
| } | ||
| const token = authHeader.slice(7); | ||
| const result = await ctx.runMutation(internal.openaiProxy.decrementToken, { token }); | ||
| if (!result.success) { | ||
| return new Response(result.error, { status: 401 }); | ||
| } | ||
|
|
||
| let body: any; | ||
| try { | ||
| body = await req.json(); | ||
| } catch (error) { | ||
| return new Response("Invalid request body", { status: 400 }); | ||
| } | ||
| if (body.model != 'gpt-4o-mini') { | ||
| return new Response("Only gpt-4o-mini is supported", { status: 400 }); | ||
| } | ||
| let body: any; | ||
| try { | ||
| body = await req.json(); | ||
| } catch (error) { | ||
| return new Response('Invalid request body', { status: 400 }); | ||
| } | ||
| if (body.model != 'gpt-4o-mini') { | ||
| return new Response('Only gpt-4o-mini is supported', { status: 400 }); | ||
| } | ||
|
|
||
| const response = await fetch("https://api.openai.com/v1/chat/completions", { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| "Authorization": `Bearer ${process.env.OPENAI_API_KEY}`, | ||
| }, | ||
| body: JSON.stringify(body), | ||
| }); | ||
| return response; | ||
| const response = await fetch('https://api.openai.com/v1/chat/completions', { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, | ||
| }, | ||
| body: JSON.stringify(body), | ||
| }); | ||
| return response; | ||
| }); | ||
|
|
||
| export const issueOpenAIToken = mutation({ | ||
| handler: async (ctx) => { | ||
| if (!openaiProxyEnabled()) { | ||
| return null; | ||
| } | ||
| const member = await getCurrentMember(ctx); | ||
| if (!member) { | ||
| console.error("Not authorized", member) | ||
| return null; | ||
| } | ||
| const existing = await ctx.db.query('memberOpenAITokens') | ||
| .withIndex('byMemberId', (q) => q.eq('memberId', member._id)) | ||
| .unique(); | ||
| if (existing) { | ||
| return existing.token; | ||
| } | ||
| const token = crypto.randomUUID(); | ||
| await ctx.db.insert('memberOpenAITokens', { | ||
| memberId: member._id, | ||
| token, | ||
| requestsRemaining: included4oMiniRequests(), | ||
| lastUsedTime: 0, | ||
| }); | ||
| return token; | ||
| handler: async (ctx) => { | ||
| if (!openaiProxyEnabled()) { | ||
| return null; | ||
| } | ||
| const member = await getCurrentMember(ctx); | ||
| if (!member) { | ||
| console.error('Not authorized', member); | ||
| return null; | ||
| } | ||
| }) | ||
| const existing = await ctx.db | ||
| .query('memberOpenAITokens') | ||
| .withIndex('byMemberId', (q) => q.eq('memberId', member._id)) | ||
| .unique(); | ||
| if (existing) { | ||
| return existing.token; | ||
| } | ||
| const token = crypto.randomUUID(); | ||
| await ctx.db.insert('memberOpenAITokens', { | ||
| memberId: member._id, | ||
| token, | ||
| requestsRemaining: included4oMiniRequests(), | ||
| lastUsedTime: 0, | ||
| }); | ||
| return token; | ||
| }, | ||
| }); | ||
|
|
||
| export const decrementToken = internalMutation({ | ||
| args: { | ||
| token: v.string(), | ||
| }, | ||
| handler: async (ctx, args) => { | ||
| if (!openaiProxyEnabled()) { | ||
| return { success: false, error: "Convex OpenAI proxy is disabled."}; | ||
| } | ||
| const token = await ctx.db.query('memberOpenAITokens') | ||
| .withIndex('byToken', (q) => q.eq('token', args.token)) | ||
| .unique(); | ||
| if (!token) { | ||
| return { success: false, error: "Invalid OPENAI_API_TOKEN" }; | ||
| } | ||
| if (token.requestsRemaining <= 0) { | ||
| return { success: false, error: "Convex OPENAI_API_TOKEN has no requests remaining. Go sign up for an OpenAI API key at https://platform.openai.com and update your app to use that." }; | ||
| } | ||
| await ctx.db.patch(token._id, { | ||
| requestsRemaining: token.requestsRemaining - 1, | ||
| lastUsedTime: Date.now(), | ||
| }); | ||
| return { success: true }; | ||
| args: { | ||
| token: v.string(), | ||
| }, | ||
| handler: async (ctx, args) => { | ||
| if (!openaiProxyEnabled()) { | ||
| return { success: false, error: 'Convex OpenAI proxy is disabled.' }; | ||
| } | ||
| const token = await ctx.db | ||
| .query('memberOpenAITokens') | ||
| .withIndex('byToken', (q) => q.eq('token', args.token)) | ||
| .unique(); | ||
| if (!token) { | ||
| return { success: false, error: 'Invalid OPENAI_API_TOKEN' }; | ||
| } | ||
| }) | ||
| if (token.requestsRemaining <= 0) { | ||
| return { | ||
| success: false, | ||
| error: | ||
| 'Convex OPENAI_API_TOKEN has no requests remaining. Go sign up for an OpenAI API key at https://platform.openai.com and update your app to use that.', | ||
| }; | ||
| } | ||
| await ctx.db.patch(token._id, { | ||
| requestsRemaining: token.requestsRemaining - 1, | ||
| lastUsedTime: Date.now(), | ||
| }); | ||
| return { success: true }; | ||
| }, | ||
| }); | ||
|
|
||
| // Cost per gpt-4o-mini request (2025-04-09): | ||
| // 16384 max output tokens @ $0.6/1M | ||
| // 128K max input tokens @ $0.15/1M | ||
| // => ~$0.03 per request. | ||
| function included4oMiniRequests() { | ||
| const fromEnv = process.env.OPENAI_PROXY_INCLUDED_REQUESTS; | ||
| if (!fromEnv) { | ||
| return 100; | ||
| } | ||
| return Number(fromEnv); | ||
| const fromEnv = process.env.OPENAI_PROXY_INCLUDED_REQUESTS; | ||
| if (!fromEnv) { | ||
| return 100; | ||
| } | ||
| return Number(fromEnv); | ||
| } | ||
|
|
||
| function openaiProxyEnabled() { | ||
| const fromEnv = process.env.OPENAI_PROXY_ENABLED; | ||
| return fromEnv && fromEnv == "1"; | ||
| } | ||
| const fromEnv = process.env.OPENAI_PROXY_ENABLED; | ||
| return fromEnv && fromEnv == '1'; | ||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tom pointed out we need to do codegen first