Skip to content

Commit 2ef9c64

Browse files
authored
Merge pull request #49 from donvito/feature/compose-feature-refactor
Feature/compose feature refactor
2 parents c7632e9 + 22a2f3b commit 2ef9c64

28 files changed

+1683
-201
lines changed

src/app.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,25 @@ function configureApiSecurity(app: OpenAPIHono, tokenConfig: string) {
3333
path === '/api/sentiment-demo' ||
3434
path === '/api/keywords-demo' ||
3535
path === '/api/email-reply-demo' ||
36+
path === '/api/rewrite-demo' ||
37+
path === '/api/compose-demo' ||
3638
path === '/api/translate-demo' ||
3739
path === '/api/meeting-notes-demo' ||
3840
path === '/api/asktext-demo' ||
3941
path === '/api/project-planner-demo' ||
42+
// Versioned demo pages (v1)
43+
path === '/api/v1/demos' ||
44+
path === '/api/v1/highlighter-demo' ||
45+
path === '/api/v1/summarize-demo' ||
46+
path === '/api/v1/sentiment-demo' ||
47+
path === '/api/v1/keywords-demo' ||
48+
path === '/api/v1/email-reply-demo' ||
49+
path === '/api/v1/rewrite-demo' ||
50+
path === '/api/v1/compose-demo' ||
51+
path === '/api/v1/translate-demo' ||
52+
path === '/api/v1/meeting-notes-demo' ||
53+
path === '/api/v1/asktext-demo' ||
54+
path === '/api/v1/project-planner-demo' ||
4055
path === '/api/models' ||
4156
path === '/api/jsoneditor' ||
4257
// Public read-only service catalog for demos
@@ -196,4 +211,4 @@ const initialize = async () => {
196211
return app;
197212
};
198213

199-
export default initialize;
214+
export default initialize;

src/config/models.json

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,65 +3,64 @@
33
"ollama": {
44
"enabled": true,
55
"models": [
6-
{ "name": "llama3.2:latest", "capabilities": ["summarize", "planning", "keywords", "sentiment", "emailReply", "translate"], "notes": "Meta Llama 3.2 general-purpose model." },
7-
{ "name": "qwen2.5-coder:latest", "capabilities": ["summarize", "planning", "keywords", "emailReply"], "notes": "Qwen 2.5 coder-optimized variant for code tasks." },
8-
{ "name": "gemma2:2b", "capabilities": ["summarize", "planning", "keywords", "sentiment", "emailReply", "meetingNotes"], "notes": "Gemma 2 small variant for lightweight tasks." },
9-
{ "name": "gemma3:4b", "capabilities": ["summarize", "planning", "keywords", "sentiment", "emailReply", "askText", "translate", "meetingNotes"], "notes": "Gemma 3 small variant for lightweight tasks with Q&A support." },
10-
{ "name": "qwen2.5:0.5b", "capabilities": ["summarize", "planning", "keywords", "sentiment", "emailReply"], "notes": "Qwen 2.5 0.5B parameter model for ultra-light workloads." },
11-
{ "name": "qwen2.5:1.5b", "capabilities": ["summarize", "planning", "keywords", "sentiment", "emailReply"], "notes": "Qwen 2.5 1.5B parameter model; balanced speed/quality." },
12-
{ "name": "qwen2.5:3b", "capabilities": ["summarize", "planning", "keywords", "sentiment", "askText", "emailReply", "translate"], "notes": "Qwen 2.5 3B parameter model for stronger quality." },
13-
{ "name": "qwen2.5:7b", "capabilities": ["summarize", "planning", "keywords", "sentiment", "emailReply"], "notes": "Qwen 2.5 7B parameter model for higher quality." },
14-
{ "name": "llama3.2:1b", "capabilities": ["summarize", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "Llama 3.2 1B tiny variant for edge/light usage." },
15-
{ "name": "llama3.2:3b", "capabilities": ["summarize", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "Llama 3.2 3B small variant." },
16-
{ "name": "gemma3:270m", "capabilities": ["summarize", "planning", "keywords", "sentiment", "askText", "emailReply"], "notes": "Gemma 3 270M instruct tuned; great for ultra-fast summarization on CPU." }
6+
{ "name": "llama3.2:latest", "capabilities": ["summarize", "rewrite", "compose", "planning", "keywords", "sentiment", "emailReply", "translate"], "notes": "Meta Llama 3.2 general-purpose model." },
7+
{ "name": "qwen2.5-coder:latest", "capabilities": ["summarize", "rewrite", "compose", "planning", "keywords", "emailReply"], "notes": "Qwen 2.5 coder-optimized variant for code tasks." },
8+
{ "name": "gemma2:2b", "capabilities": ["summarize", "rewrite", "compose", "planning", "keywords", "sentiment", "emailReply", "meetingNotes"], "notes": "Gemma 2 small variant for lightweight tasks." },
9+
{ "name": "gemma3:4b", "capabilities": ["summarize", "rewrite", "compose", "planning", "keywords", "sentiment", "emailReply", "askText", "translate", "meetingNotes"], "notes": "Gemma 3 small variant for lightweight tasks with Q&A support." },
10+
{ "name": "qwen2.5:0.5b", "capabilities": ["summarize", "rewrite", "compose", "planning", "keywords", "sentiment", "emailReply"], "notes": "Qwen 2.5 0.5B parameter model for ultra-light workloads." },
11+
{ "name": "qwen2.5:1.5b", "capabilities": ["summarize", "rewrite", "compose", "planning", "keywords", "sentiment", "emailReply"], "notes": "Qwen 2.5 1.5B parameter model; balanced speed/quality." },
12+
{ "name": "qwen2.5:3b", "capabilities": ["summarize", "rewrite", "compose", "planning", "keywords", "sentiment", "askText", "emailReply", "translate"], "notes": "Qwen 2.5 3B parameter model for stronger quality." },
13+
{ "name": "qwen2.5:7b", "capabilities": ["summarize", "rewrite", "compose", "planning", "keywords", "sentiment", "emailReply"], "notes": "Qwen 2.5 7B parameter model for higher quality." },
14+
{ "name": "llama3.2:1b", "capabilities": ["summarize", "rewrite", "compose", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "Llama 3.2 1B tiny variant for edge/light usage." },
15+
{ "name": "llama3.2:3b", "capabilities": ["summarize", "rewrite", "compose", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "Llama 3.2 3B small variant." },
16+
{ "name": "gemma3:270m", "capabilities": ["summarize", "rewrite", "compose", "planning", "keywords", "sentiment", "askText", "emailReply"], "notes": "Gemma 3 270M instruct tuned; great for ultra-fast summarization on CPU." }
1717
]
1818
},
1919
"lmstudio": {
2020
"enabled": true,
2121
"models": [
22-
{ "name": "gemma-3-270m-it", "capabilities": ["summarize", "planning", "keywords", "sentiment", "askText", "emailReply"], "notes": "Gemma 3 270M instruct tuned; great for ultra-fast summarization on CPU." },
23-
{ "name": "llama-3.2-3b-instruct", "capabilities": ["summarize", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "Llama 3.2 3B instruct; strong small model for summarization." }
22+
{ "name": "gemma-3-270m-it", "capabilities": ["summarize", "rewrite", "compose", "planning", "keywords", "sentiment", "askText", "emailReply"], "notes": "Gemma 3 270M instruct tuned; great for ultra-fast summarization on CPU." },
23+
{ "name": "llama-3.2-3b-instruct", "capabilities": ["summarize", "rewrite", "compose", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "Llama 3.2 3B instruct; strong small model for summarization." }
2424
]
2525
},
2626
"openai": {
2727
"enabled": true,
2828
"models": [
29-
{ "name": "gpt-4o-mini", "capabilities": ["summarize", "planning", "keywords", "sentiment", "vision", "emailReply", "translate", "meetingNotes"], "notes": "OpenAI multimodal small model with vision support." },
30-
{ "name": "gpt-4.1-nano", "capabilities": ["summarize", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "OpenAI lightweight model for fast, low-cost text tasks." },
31-
{ "name": "gpt-5-mini", "capabilities": ["summarize", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "OpenAI next-gen mini model with Q&A capabilities." },
32-
{ "name": "gpt-5-nano", "capabilities": ["summarize", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "OpenAI next-gen nano model with Q&A capabilities." },
33-
{ "name": "gpt-5", "capabilities": ["summarize", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "OpenAI next-gen model with Q&A capabilities." },
34-
{ "name": "gpt-4o", "capabilities": ["summarize", "planning", "keywords", "sentiment", "vision", "emailReply", "translate", "meetingNotes"], "notes": "OpenAI next-gen vision model with Q&A capabilities." }
29+
{ "name": "gpt-4o-mini", "capabilities": ["summarize", "rewrite", "compose", "planning", "keywords", "sentiment", "vision", "emailReply", "translate", "meetingNotes"], "notes": "OpenAI multimodal small model with vision support." },
30+
{ "name": "gpt-4.1-nano", "capabilities": ["summarize", "rewrite", "compose", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "OpenAI lightweight model for fast, low-cost text tasks." },
31+
{ "name": "gpt-5-mini", "capabilities": ["summarize", "rewrite", "compose", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "OpenAI next-gen mini model with Q&A capabilities." },
32+
{ "name": "gpt-5-nano", "capabilities": ["summarize", "rewrite", "compose", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "OpenAI next-gen nano model with Q&A capabilities." },
33+
{ "name": "gpt-5", "capabilities": ["summarize", "rewrite", "compose", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "OpenAI next-gen model with Q&A capabilities." },
34+
{ "name": "gpt-4o", "capabilities": ["summarize", "rewrite", "compose", "planning", "keywords", "sentiment", "vision", "emailReply", "translate", "meetingNotes"], "notes": "OpenAI next-gen vision model with Q&A capabilities." }
3535
]
3636
},
3737
"openrouter": {
3838
"enabled": true,
3939
"models": [
40-
{ "name": "anthropic/claude-3.5-sonnet", "capabilities": ["summarize", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "Claude 3.5 Sonnet via OpenRouter; strong reasoning." },
41-
{ "name": "openai/gpt-4o-mini", "capabilities": ["summarize", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "OpenRouter proxy to GPT-4o mini." },
42-
{ "name": "google/gemini-2.0-flash-lite-001", "capabilities": ["summarize", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "Google Gemini 2.0 Flash Lite via OpenRouter with Q&A capabilities." },
43-
{ "name": "google/gemini-2.5-flash-lite", "capabilities": ["summarize", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "Google Gemini 2.5 Flash Lite via OpenRouter with 1M token context window for large document Q&A." },
44-
{ "name": "openai/gpt-oss-20b", "capabilities": ["summarize", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "OpenRouter proxy to GPT OSS 20B" },
45-
{ "name": "openai/gpt-oss-120b", "capabilities": ["summarize", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "OpenRouter proxy to GPT OSS 120B" }
40+
{ "name": "anthropic/claude-3.5-sonnet", "capabilities": ["summarize", "rewrite", "compose", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "Claude 3.5 Sonnet via OpenRouter; strong reasoning." },
41+
{ "name": "openai/gpt-4o-mini", "capabilities": ["summarize", "rewrite", "compose", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "OpenRouter proxy to GPT-4o mini." },
42+
{ "name": "google/gemini-2.0-flash-lite-001", "capabilities": ["summarize", "rewrite", "compose", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "Google Gemini 2.0 Flash Lite via OpenRouter with Q&A capabilities." },
43+
{ "name": "google/gemini-2.5-flash-lite", "capabilities": ["summarize", "rewrite", "compose", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "Google Gemini 2.5 Flash Lite via OpenRouter with 1M token context window for large document Q&A." },
44+
{ "name": "openai/gpt-oss-20b", "capabilities": ["summarize", "rewrite", "compose", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "OpenRouter proxy to GPT OSS 20B" },
45+
{ "name": "openai/gpt-oss-120b", "capabilities": ["summarize", "rewrite", "compose", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "OpenRouter proxy to GPT OSS 120B" }
4646
]
4747
},
4848
"anthropic": {
4949
"enabled": true,
5050
"models": [
51-
{ "name": "claude-3-haiku-20240307", "capabilities": ["summarize", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "Anthropic Claude 3 Haiku; fast and cost-effective with Q&A support." }
51+
{ "name": "claude-3-haiku-20240307", "capabilities": ["summarize", "rewrite", "compose", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "Anthropic Claude 3 Haiku; fast and cost-effective with Q&A support." }
5252
]
5353
},
5454
"aigateway": {
5555
"enabled": true,
5656
"models": [
57-
{ "name": "openai/gpt-5-nano", "capabilities": ["summarize", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "GPT-5 nano is a high throughput model that excels at simple instruction or classification tasks." },
58-
{ "name": "gemini-2.0-flash-lite-preview-02-05", "capabilities": ["summarize", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "Gemini 2.0 Flash Lite Preview 02-05" },
59-
{ "name": "mistral-small-2503", "capabilities": ["summarize", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "Mistral Small" }
57+
{ "name": "openai/gpt-5-nano", "capabilities": ["summarize", "rewrite", "compose", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "GPT-5 nano is a high throughput model that excels at simple instruction or classification tasks." },
58+
{ "name": "gemini-2.0-flash-lite-preview-02-05", "capabilities": ["summarize", "rewrite", "compose", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "Gemini 2.0 Flash Lite Preview 02-05" },
59+
{ "name": "mistral-small-2503", "capabilities": ["summarize", "rewrite", "compose", "planning", "keywords", "sentiment", "askText", "emailReply", "translate", "meetingNotes"], "notes": "Mistral Small" }
6060

6161

6262
]
6363
}
6464
}
6565
}
6666

67-

src/config/models.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import * as path from 'path'
33

44
export type ModelCapability =
55
| 'summarize'
6+
| 'rewrite'
7+
| 'compose'
68
| 'keywords'
79
| 'sentiment'
810
| 'planning'
@@ -66,4 +68,3 @@ export function getModelsCatalogByProvider(): Record<string, ProviderModelConfig
6668
return result
6769
}
6870

69-
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,4 +418,4 @@ router.openapi(jsonEditorRoute, (c) => {
418418
export default {
419419
handler: router,
420420
mountPath: 'jsoneditor'
421-
};
421+
};

src/routes/v1/compose.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi'
2+
import { Context } from 'hono'
3+
import { streamSSE } from 'hono/streaming'
4+
import { composePrompt } from '../../utils/prompts'
5+
import { handleError } from '../../utils/errorHandler'
6+
import { composeRequestSchema, composeResponseSchema, createComposeResponse } from '../../schemas/v1/compose'
7+
import { processTextOutputRequest } from '../../services/ai'
8+
import { apiVersion } from './versionConfig'
9+
import { createFinalResponse } from './finalResponse'
10+
11+
const router = new OpenAPIHono()
12+
13+
async function handleComposeRequest(c: Context) {
14+
try {
15+
const { payload, config } = await c.req.json()
16+
const provider = config.provider
17+
const model = config.model
18+
const isStreaming = config.stream || false
19+
20+
const prompt = composePrompt(payload.topic, payload.maxLength)
21+
22+
if (isStreaming) {
23+
const result = await processTextOutputRequest(prompt, config)
24+
25+
c.header('Content-Type', 'text/event-stream')
26+
c.header('Cache-Control', 'no-cache')
27+
c.header('Connection', 'keep-alive')
28+
29+
return streamSSE(c, async (stream) => {
30+
try {
31+
const textStream = result.textStream
32+
if (!textStream) {
33+
throw new Error('Streaming not supported for this provider/model')
34+
}
35+
36+
for await (const chunk of textStream) {
37+
await stream.writeSSE({
38+
data: JSON.stringify({
39+
chunk: chunk,
40+
provider: provider,
41+
model: model,
42+
version: apiVersion
43+
})
44+
})
45+
}
46+
47+
const usage = await result.usage
48+
if (usage) {
49+
await stream.writeSSE({
50+
data: JSON.stringify({
51+
done: true,
52+
usage: {
53+
input_tokens: usage.promptTokens,
54+
output_tokens: usage.completionTokens,
55+
total_tokens: usage.totalTokens
56+
},
57+
provider: provider,
58+
model: model,
59+
version: apiVersion
60+
})
61+
})
62+
}
63+
} catch (error) {
64+
try {
65+
await stream.writeSSE({
66+
data: JSON.stringify({
67+
error: error instanceof Error ? error.message : 'Streaming error',
68+
done: true
69+
})
70+
})
71+
} catch {}
72+
} finally {
73+
try { await stream.close() } catch {}
74+
}
75+
})
76+
}
77+
78+
const result = await processTextOutputRequest(prompt, config)
79+
const finalResponse = createComposeResponse(result.text, provider, model, {
80+
input_tokens: result.usage.promptTokens,
81+
output_tokens: result.usage.completionTokens,
82+
total_tokens: result.usage.totalTokens,
83+
})
84+
85+
const finalResponseWithVersion = createFinalResponse(finalResponse, apiVersion)
86+
return c.json(finalResponseWithVersion, 200)
87+
} catch (error) {
88+
return handleError(c, error, 'Failed to compose text')
89+
}
90+
}
91+
92+
router.openapi(
93+
createRoute({
94+
path: '/',
95+
method: 'post',
96+
security: [ { BearerAuth: [] } ],
97+
request: {
98+
body: {
99+
content: {
100+
'application/json': {
101+
schema: composeRequestSchema
102+
}
103+
}
104+
}
105+
},
106+
responses: {
107+
200: {
108+
description: 'Returns composed text for the provided topic.',
109+
content: {
110+
'application/json': {
111+
schema: composeResponseSchema
112+
}
113+
}
114+
},
115+
401: {
116+
description: 'Unauthorized - Bearer token required',
117+
content: {
118+
'application/json': {
119+
schema: z.object({
120+
error: z.string()
121+
})
122+
}
123+
}
124+
}
125+
},
126+
summary: 'Compose text',
127+
description: 'This endpoint receives a topic and uses an LLM to compose text about it.',
128+
tags: ['API']
129+
}),
130+
handleComposeRequest as any
131+
)
132+
133+
export default {
134+
handler: router,
135+
mountPath: 'compose'
136+
}

0 commit comments

Comments
 (0)