Vercel ai gateway (#7)
* feat: use vercel ai gateway * chore: update UI
This commit is contained in:
@@ -1,3 +1,2 @@
|
|||||||
DATABASE_URL=""
|
DATABASE_URL=""
|
||||||
USER_KEY=""
|
USER_KEY=""
|
||||||
ANTHROPIC_API_KEY=""
|
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
DATABASE_URL=""
|
DATABASE_URL=""
|
||||||
USER_KEY="test-key-123"
|
USER_KEY="test-key-123"
|
||||||
ANTHROPIC_API_KEY=""
|
|
||||||
@@ -4,12 +4,7 @@
|
|||||||
"es2021": true,
|
"es2021": true,
|
||||||
"jest": true
|
"jest": true
|
||||||
},
|
},
|
||||||
"extends": [
|
"extends": ["next/core-web-vitals", "eslint:recommended", "prettier"],
|
||||||
"next/core-web-vitals",
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"prettier"
|
|
||||||
],
|
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": "latest",
|
"ecmaVersion": "latest",
|
||||||
@@ -18,6 +13,6 @@
|
|||||||
"plugins": ["@typescript-eslint"],
|
"plugins": ["@typescript-eslint"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"@typescript-eslint/no-unused-vars": "error",
|
"@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
|
- **Framework**: Next.js 15
|
||||||
- **Language**: TypeScript
|
- **Language**: TypeScript
|
||||||
- **AI Integration**: Anthropic's Claude API
|
- **AI Integration**: Anthropic API via Vercel AI Gateway
|
||||||
- **Testing**: Jest
|
- **Testing**: Jest
|
||||||
- **Deployment**: Vercel
|
- **Deployment**: Vercel
|
||||||
- **Code Quality**: ESLint, Prettier, Husky
|
- **Code Quality**: ESLint, Prettier, Husky
|
||||||
@@ -32,9 +32,7 @@ A versatile backend service that extends Siri's capabilities through custom shor
|
|||||||
```
|
```
|
||||||
|
|
||||||
Fill in:
|
Fill in:
|
||||||
|
|
||||||
- `USER_KEY`: Your API authentication key
|
- `USER_KEY`: Your API authentication key
|
||||||
- `ANTHROPIC_API_KEY`: Your Anthropic API key for Claude AI
|
|
||||||
|
|
||||||
3. **Run development server**
|
3. **Run development server**
|
||||||
|
|
||||||
|
|||||||
@@ -3,16 +3,35 @@ import { ShortcutsHandler } from '@utils/handler';
|
|||||||
import { RequestSchema } from '@utils/types';
|
import { RequestSchema } from '@utils/types';
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
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 {
|
try {
|
||||||
const body = await req.json();
|
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);
|
const result = RequestSchema.safeParse(body);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
|
console.warn(
|
||||||
|
`[${requestId}] Invalid request format:`,
|
||||||
|
result.error.issues
|
||||||
|
);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Invalid request format.',
|
message: 'Invalid request format.',
|
||||||
errors: result.error.errors
|
errors: result.error.issues
|
||||||
},
|
},
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
@@ -22,6 +41,9 @@ export async function POST(req: Request) {
|
|||||||
const isValid = shortcutsHandler.validateRequest(result.data);
|
const isValid = shortcutsHandler.validateRequest(result.data);
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
|
console.warn(
|
||||||
|
`[${requestId}] Unauthorized request for command: ${result.data.command}`
|
||||||
|
);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
success: false,
|
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(
|
const response = await shortcutsHandler.processCommand(
|
||||||
result.data.command,
|
result.data.command,
|
||||||
result.data.parameters
|
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);
|
return NextResponse.json(response);
|
||||||
} catch (error) {
|
} 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(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'DiaryWhisper',
|
title: 'DiaryWhisper',
|
||||||
description: 'Siri-enabled diary tracker for expenses and day logs'
|
description: 'Siri-enabled diary tracker for expenses and day logs'
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({ children }: { children: ReactNode }) {
|
||||||
children
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<html lang='en'>
|
<html lang='en'>
|
||||||
<body>{children}</body>
|
<body>{children}</body>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export default function Home() {
|
|||||||
<>
|
<>
|
||||||
<div className='container'>
|
<div className='container'>
|
||||||
<div className='header'>
|
<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>
|
<p>Anthropic-based power up for Siri Shortcuts</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ export default function Home() {
|
|||||||
|
|
||||||
<div className='status'>
|
<div className='status'>
|
||||||
<span>Status: OPERATIONAL</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
29
package.json
29
package.json
@@ -24,36 +24,43 @@
|
|||||||
"db:migrate": "prisma migrate dev"
|
"db:migrate": "prisma migrate dev"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.33.1",
|
|
||||||
"@prisma/client": "^5.22.0",
|
"@prisma/client": "^5.22.0",
|
||||||
|
"ai": "^5.0.68",
|
||||||
"axios": "^1.12.0",
|
"axios": "^1.12.0",
|
||||||
"next": "^15.4.7",
|
"next": "^15.4.7",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"zod": "^3.24.1"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^19.6.1",
|
"@commitlint/cli": "^19.6.1",
|
||||||
"@commitlint/config-conventional": "^19.6.0",
|
"@commitlint/config-conventional": "^19.6.0",
|
||||||
"@types/jest": "^29.5.14",
|
"@eslint/compat": "^1.4.0",
|
||||||
"@types/node": "^22.10.2",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
|
"@types/jest": "^30.0.0",
|
||||||
|
"@types/node": "^24.7.1",
|
||||||
"@types/react": "^19.0.2",
|
"@types/react": "^19.0.2",
|
||||||
"@types/react-dom": "^19.0.2",
|
"@types/react-dom": "^19.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.18.2",
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
"@typescript-eslint/parser": "^8.18.2",
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^17.2.3",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-next": "^15.1.3",
|
"eslint-config-next": "^15.5.4",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"globals": "^16.4.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"jest": "^29.7.0",
|
"jest": "^30.2.0",
|
||||||
"lint-staged": "^15.3.0",
|
"lint-staged": "^15.3.0",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"prisma": "^5.22.0",
|
"prisma": "^5.22.0",
|
||||||
"ts-jest": "^29.2.5",
|
"ts-jest": "^29.4.5",
|
||||||
"ts-node": "^10.9.2",
|
"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": {
|
"lint-staged": {
|
||||||
"*.{ts,tsx}": [
|
"*.{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 { ShortcutsResponse } from '../types';
|
||||||
import { dbOperations } from '@utils/db';
|
import { dbOperations } from '@utils/db';
|
||||||
|
|
||||||
export async function anthropicCommand(
|
export async function anthropicCommand(
|
||||||
parameters: Record<string, string> | undefined
|
parameters: Record<string, string> | undefined
|
||||||
): Promise<ShortcutsResponse> {
|
): Promise<ShortcutsResponse> {
|
||||||
|
const commandId = Math.random().toString(36).substring(7);
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
let question = '';
|
let question = '';
|
||||||
let response = '';
|
let response = '';
|
||||||
let success = false;
|
let success = false;
|
||||||
let errorMessage: string | undefined;
|
let errorMessage: string | undefined;
|
||||||
let tokensUsed: number | undefined;
|
let tokensUsed: number | undefined;
|
||||||
|
|
||||||
|
console.info(`[CMD-${commandId}] Anthropic command started`, {
|
||||||
|
hasParameters: !!parameters,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!parameters || !parameters['question']) {
|
if (!parameters || !parameters['question']) {
|
||||||
|
console.warn(`[CMD-${commandId}] Missing question parameter`);
|
||||||
errorMessage = 'Need to provide a question.';
|
errorMessage = 'Need to provide a question.';
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -21,16 +30,35 @@ export async function anthropicCommand(
|
|||||||
}
|
}
|
||||||
|
|
||||||
question = parameters['question'];
|
question = parameters['question'];
|
||||||
|
console.info(`[CMD-${commandId}] Processing question`, {
|
||||||
|
questionLength: question.length,
|
||||||
|
question:
|
||||||
|
question.substring(0, 100) + (question.length > 100 ? '...' : '')
|
||||||
|
});
|
||||||
|
|
||||||
const prompt =
|
const prompt =
|
||||||
'I want to know ' +
|
'I want to know ' +
|
||||||
question +
|
question +
|
||||||
'. Structure the response in a manner suitable for spoken communication.';
|
'. 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;
|
response = anthropicResponse.text;
|
||||||
tokensUsed = anthropicResponse.tokensUsed;
|
tokensUsed = anthropicResponse.tokensUsed;
|
||||||
success = true;
|
success = true;
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
console.info(
|
||||||
|
`[CMD-${commandId}] Anthropic command completed in ${duration}ms`,
|
||||||
|
{
|
||||||
|
responseLength: response.length,
|
||||||
|
tokensUsed,
|
||||||
|
success: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: response,
|
message: response,
|
||||||
@@ -39,7 +67,11 @@ export async function anthropicCommand(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} 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;
|
success = false;
|
||||||
errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
response = 'Sorry. There was a problem with Anthropic.';
|
response = 'Sorry. There was a problem with Anthropic.';
|
||||||
@@ -51,6 +83,7 @@ export async function anthropicCommand(
|
|||||||
} finally {
|
} finally {
|
||||||
if (question) {
|
if (question) {
|
||||||
try {
|
try {
|
||||||
|
console.info(`[CMD-${commandId}] Saving query to database`);
|
||||||
await dbOperations.saveAnthropicQuery({
|
await dbOperations.saveAnthropicQuery({
|
||||||
question,
|
question,
|
||||||
response,
|
response,
|
||||||
@@ -58,8 +91,12 @@ export async function anthropicCommand(
|
|||||||
errorMessage,
|
errorMessage,
|
||||||
tokensUsed
|
tokensUsed
|
||||||
});
|
});
|
||||||
|
console.info(`[CMD-${commandId}] Query saved to database successfully`);
|
||||||
} catch (error) {
|
} 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({
|
export const RequestSchema = z.object({
|
||||||
command: z.string(),
|
command: z.string(),
|
||||||
parameters: z.record(z.string()).optional(),
|
parameters: z.record(z.string(), z.string()).optional(),
|
||||||
apiKey: z.string()
|
apiKey: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user