Vercel ai gateway (#7)
* feat: use vercel ai gateway * chore: update UI
This commit is contained in:
@@ -1,3 +1,2 @@
|
||||
DATABASE_URL=""
|
||||
USER_KEY=""
|
||||
ANTHROPIC_API_KEY=""
|
||||
@@ -4,12 +4,7 @@
|
||||
"es2021": true,
|
||||
"jest": true
|
||||
},
|
||||
"extends": [
|
||||
"next/core-web-vitals",
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"extends": ["next/core-web-vitals", "eslint:recommended", "prettier"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
@@ -18,6 +13,6 @@
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/consistent-type-definitions": "error"
|
||||
"no-console": "off"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ A versatile backend service that extends Siri's capabilities through custom shor
|
||||
|
||||
- **Framework**: Next.js 15
|
||||
- **Language**: TypeScript
|
||||
- **AI Integration**: Anthropic's Claude API
|
||||
- **AI Integration**: Anthropic API via Vercel AI Gateway
|
||||
- **Testing**: Jest
|
||||
- **Deployment**: Vercel
|
||||
- **Code Quality**: ESLint, Prettier, Husky
|
||||
@@ -32,9 +32,7 @@ A versatile backend service that extends Siri's capabilities through custom shor
|
||||
```
|
||||
|
||||
Fill in:
|
||||
|
||||
- `USER_KEY`: Your API authentication key
|
||||
- `ANTHROPIC_API_KEY`: Your Anthropic API key for Claude AI
|
||||
|
||||
3. **Run development server**
|
||||
|
||||
|
||||
@@ -3,16 +3,35 @@ import { ShortcutsHandler } from '@utils/handler';
|
||||
import { RequestSchema } from '@utils/types';
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const requestId = Math.random().toString(36).substring(7);
|
||||
const startTime = Date.now();
|
||||
|
||||
console.info(
|
||||
`[${requestId}] Incoming request at ${new Date().toISOString()}`
|
||||
);
|
||||
|
||||
try {
|
||||
const body = await req.json();
|
||||
console.info(`[${requestId}] Request body:`, {
|
||||
command: body.command,
|
||||
hasParameters: !!body.parameters,
|
||||
parametersCount: body.parameters
|
||||
? Object.keys(body.parameters).length
|
||||
: 0,
|
||||
hasApiKey: !!body.apiKey
|
||||
});
|
||||
|
||||
const result = RequestSchema.safeParse(body);
|
||||
if (!result.success) {
|
||||
console.warn(
|
||||
`[${requestId}] Invalid request format:`,
|
||||
result.error.issues
|
||||
);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: 'Invalid request format.',
|
||||
errors: result.error.errors
|
||||
errors: result.error.issues
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
@@ -22,6 +41,9 @@ export async function POST(req: Request) {
|
||||
const isValid = shortcutsHandler.validateRequest(result.data);
|
||||
|
||||
if (!isValid) {
|
||||
console.warn(
|
||||
`[${requestId}] Unauthorized request for command: ${result.data.command}`
|
||||
);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
@@ -31,14 +53,25 @@ export async function POST(req: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
console.info(`[${requestId}] Processing command: ${result.data.command}`);
|
||||
const response = await shortcutsHandler.processCommand(
|
||||
result.data.command,
|
||||
result.data.parameters
|
||||
);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
console.info(`[${requestId}] Request completed in ${duration}ms`, {
|
||||
success: response.success,
|
||||
hasData: !!response.data
|
||||
});
|
||||
|
||||
return NextResponse.json(response);
|
||||
} catch (error) {
|
||||
console.error('Error processing shortcuts request:', error);
|
||||
const duration = Date.now() - startTime;
|
||||
console.error(
|
||||
`[${requestId}] Error processing request after ${duration}ms:`,
|
||||
error
|
||||
);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'DiaryWhisper',
|
||||
description: 'Siri-enabled diary tracker for expenses and day logs'
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
export default function RootLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<html lang='en'>
|
||||
<body>{children}</body>
|
||||
|
||||
@@ -7,7 +7,7 @@ export default function Home() {
|
||||
<>
|
||||
<div className='container'>
|
||||
<div className='header'>
|
||||
<h1>Siri Shortcuts v1.0.0</h1>
|
||||
<h1>Siri Shortcuts v1.1.0</h1>
|
||||
<p>Anthropic-based power up for Siri Shortcuts</p>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,7 @@ export default function Home() {
|
||||
|
||||
<div className='status'>
|
||||
<span>Status: OPERATIONAL</span>
|
||||
<span>Last Backup: 2024-01-18 14:30 UTC</span>
|
||||
<span>Last Backup: 2025-10-11 10:00 UTC</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
29
package.json
29
package.json
@@ -24,36 +24,43 @@
|
||||
"db:migrate": "prisma migrate dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.33.1",
|
||||
"@prisma/client": "^5.22.0",
|
||||
"ai": "^5.0.68",
|
||||
"axios": "^1.12.0",
|
||||
"next": "^15.4.7",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"zod": "^3.24.1"
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^19.6.1",
|
||||
"@commitlint/config-conventional": "^19.6.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.10.2",
|
||||
"@eslint/compat": "^1.4.0",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^24.7.1",
|
||||
"@types/react": "^19.0.2",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.18.2",
|
||||
"@typescript-eslint/parser": "^8.18.2",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"dotenv": "^16.4.7",
|
||||
"dotenv": "^17.2.3",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-next": "^15.1.3",
|
||||
"eslint-config-next": "^15.5.4",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"globals": "^16.4.0",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.7.0",
|
||||
"jest": "^30.2.0",
|
||||
"lint-staged": "^15.3.0",
|
||||
"prettier": "^3.4.2",
|
||||
"prisma": "^5.22.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-jest": "^29.4.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.7.2"
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"@babel/helpers": ">=7.26.10",
|
||||
"brace-expansion": ">=1.1.12"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx}": [
|
||||
|
||||
20
test-request.js
Normal file
20
test-request.js
Normal file
@@ -0,0 +1,20 @@
|
||||
require('dotenv').config();
|
||||
const fetch = require('axios');
|
||||
|
||||
async function testAPI() {
|
||||
try {
|
||||
const response = await fetch.post('http://localhost:3000/api/shortcut', {
|
||||
command: 'anthropic',
|
||||
parameters: {
|
||||
question: 'What is 42?'
|
||||
},
|
||||
apiKey: process.env.USER_KEY
|
||||
});
|
||||
|
||||
console.log('Success:', response.data);
|
||||
} catch (error) {
|
||||
console.error('Error:', error.response?.data || error.message);
|
||||
}
|
||||
}
|
||||
|
||||
testAPI();
|
||||
54
utils/aiGatewayClient.ts
Normal file
54
utils/aiGatewayClient.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { generateText } from 'ai';
|
||||
|
||||
interface AiGatewayResponse {
|
||||
text: string;
|
||||
tokensUsed: number;
|
||||
}
|
||||
|
||||
export async function queryAiGateway(
|
||||
text: string,
|
||||
model: string
|
||||
): Promise<AiGatewayResponse> {
|
||||
const requestId = Math.random().toString(36).substring(7);
|
||||
const startTime = Date.now();
|
||||
|
||||
console.info(`[AI-${requestId}] Starting Vercel Gateway AI request`, {
|
||||
promptLength: text.length,
|
||||
model,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await generateText({
|
||||
model,
|
||||
prompt: text
|
||||
});
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
const tokensUsed = response.usage?.totalTokens || 0;
|
||||
|
||||
console.info(
|
||||
`[AI-${requestId}] Vercel Gateway AI response received in ${duration}ms`,
|
||||
{
|
||||
responseLength: response.text.length,
|
||||
tokensUsed,
|
||||
usage: response.usage
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
text: response.text,
|
||||
tokensUsed
|
||||
};
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
console.error(
|
||||
`[AI-${requestId}] Vercel Gateway AI error after ${duration}ms:`,
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
promptLength: text.length
|
||||
}
|
||||
);
|
||||
throw new Error(`Vercel Gateway AI error: ${JSON.stringify(error)}.`);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
|
||||
interface AnthropicResponse {
|
||||
text: string;
|
||||
tokensUsed: number;
|
||||
}
|
||||
|
||||
export async function getMessage(text: string): Promise<AnthropicResponse> {
|
||||
const anthropic = new Anthropic({
|
||||
apiKey: process.env.ANTHROPIC_API_KEY
|
||||
});
|
||||
|
||||
console.info('Anthropic request with text: ', text);
|
||||
|
||||
const response = await anthropic.messages.create({
|
||||
model: 'claude-3-5-sonnet-20241022',
|
||||
max_tokens: 2048,
|
||||
messages: [{ role: 'user', content: text }]
|
||||
});
|
||||
|
||||
console.info('Anthropic response: ', response);
|
||||
|
||||
try {
|
||||
const data = response.content as [{ type: string; text: string }];
|
||||
|
||||
const tokensUsed =
|
||||
(response.usage?.input_tokens || 0) +
|
||||
(response.usage?.output_tokens || 0);
|
||||
|
||||
console.info('Token usage:', {
|
||||
input_tokens: response.usage?.input_tokens,
|
||||
output_tokens: response.usage?.output_tokens,
|
||||
total: tokensUsed
|
||||
});
|
||||
|
||||
return {
|
||||
text: data[0].text,
|
||||
tokensUsed
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`Anthropic client error: ${JSON.stringify(error)}.`);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,27 @@
|
||||
import { getMessage } from '@utils/anthropicClient';
|
||||
import { queryAiGateway } from '@utils/aiGatewayClient';
|
||||
import { ShortcutsResponse } from '../types';
|
||||
import { dbOperations } from '@utils/db';
|
||||
|
||||
export async function anthropicCommand(
|
||||
parameters: Record<string, string> | undefined
|
||||
): Promise<ShortcutsResponse> {
|
||||
const commandId = Math.random().toString(36).substring(7);
|
||||
const startTime = Date.now();
|
||||
|
||||
let question = '';
|
||||
let response = '';
|
||||
let success = false;
|
||||
let errorMessage: string | undefined;
|
||||
let tokensUsed: number | undefined;
|
||||
|
||||
console.info(`[CMD-${commandId}] Anthropic command started`, {
|
||||
hasParameters: !!parameters,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
try {
|
||||
if (!parameters || !parameters['question']) {
|
||||
console.warn(`[CMD-${commandId}] Missing question parameter`);
|
||||
errorMessage = 'Need to provide a question.';
|
||||
return {
|
||||
success: false,
|
||||
@@ -21,16 +30,35 @@ export async function anthropicCommand(
|
||||
}
|
||||
|
||||
question = parameters['question'];
|
||||
console.info(`[CMD-${commandId}] Processing question`, {
|
||||
questionLength: question.length,
|
||||
question:
|
||||
question.substring(0, 100) + (question.length > 100 ? '...' : '')
|
||||
});
|
||||
|
||||
const prompt =
|
||||
'I want to know ' +
|
||||
question +
|
||||
'. Structure the response in a manner suitable for spoken communication.';
|
||||
|
||||
const anthropicResponse = await getMessage(prompt);
|
||||
const anthropicResponse = await queryAiGateway(
|
||||
prompt,
|
||||
'anthropic/claude-sonnet-4.5'
|
||||
);
|
||||
response = anthropicResponse.text;
|
||||
tokensUsed = anthropicResponse.tokensUsed;
|
||||
success = true;
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
console.info(
|
||||
`[CMD-${commandId}] Anthropic command completed in ${duration}ms`,
|
||||
{
|
||||
responseLength: response.length,
|
||||
tokensUsed,
|
||||
success: true
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: response,
|
||||
@@ -39,7 +67,11 @@ export async function anthropicCommand(
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Anthropic command error:', error);
|
||||
const duration = Date.now() - startTime;
|
||||
console.error(
|
||||
`[CMD-${commandId}] Anthropic command failed after ${duration}ms:`,
|
||||
error
|
||||
);
|
||||
success = false;
|
||||
errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
response = 'Sorry. There was a problem with Anthropic.';
|
||||
@@ -51,6 +83,7 @@ export async function anthropicCommand(
|
||||
} finally {
|
||||
if (question) {
|
||||
try {
|
||||
console.info(`[CMD-${commandId}] Saving query to database`);
|
||||
await dbOperations.saveAnthropicQuery({
|
||||
question,
|
||||
response,
|
||||
@@ -58,8 +91,12 @@ export async function anthropicCommand(
|
||||
errorMessage,
|
||||
tokensUsed
|
||||
});
|
||||
console.info(`[CMD-${commandId}] Query saved to database successfully`);
|
||||
} catch (error) {
|
||||
console.error('Failed to log query to database:', error);
|
||||
console.error(
|
||||
`[CMD-${commandId}] Failed to log query to database:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
||||
|
||||
export const RequestSchema = z.object({
|
||||
command: z.string(),
|
||||
parameters: z.record(z.string()).optional(),
|
||||
parameters: z.record(z.string(), z.string()).optional(),
|
||||
apiKey: z.string()
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user