Skip to content

Commit edd498a

Browse files
committed
refactor(web): extract v1 route helpers
- Add shared helpers for JSON parsing, auth, and credit charging - Refactor docs-search and web-search routes to inject typed server env deps
1 parent e33c122 commit edd498a

File tree

5 files changed

+383
-320
lines changed

5 files changed

+383
-320
lines changed

packages/agent-runtime/src/llm-api/codebuff-web-api.ts

Lines changed: 91 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,47 @@ interface CodebuffWebApiEnv {
99
ciEnv: CiEnv
1010
}
1111

12-
export async function callWebSearchAPI(params: {
13-
query: string
14-
depth?: 'standard' | 'deep'
15-
repoUrl?: string | null
12+
const tryParseJson = (text: string): unknown => {
13+
try {
14+
return JSON.parse(text)
15+
} catch {
16+
return null
17+
}
18+
}
19+
20+
const getStringField = (value: unknown, key: string): string | undefined => {
21+
if (!value || typeof value !== 'object') return undefined
22+
const record = value as Record<string, unknown>
23+
const field = record[key]
24+
return typeof field === 'string' ? field : undefined
25+
}
26+
27+
const getNumberField = (value: unknown, key: string): number | undefined => {
28+
if (!value || typeof value !== 'object') return undefined
29+
const record = value as Record<string, unknown>
30+
const field = record[key]
31+
return typeof field === 'number' ? field : undefined
32+
}
33+
34+
const callCodebuffV1 = async (params: {
35+
endpoint: '/api/v1/web-search' | '/api/v1/docs-search'
36+
payload: unknown
1637
fetch: typeof globalThis.fetch
1738
logger: Logger
1839
env: CodebuffWebApiEnv
1940
baseUrl?: string
2041
apiKey?: string
21-
}): Promise<{ result?: string; error?: string; creditsUsed?: number }> {
22-
const { query, depth = 'standard', repoUrl, fetch, logger, env } = params
42+
requestName: 'web-search' | 'docs-search'
43+
}): Promise<{ json?: unknown; error?: string; creditsUsed?: number }> => {
44+
const { endpoint, payload, fetch, logger, env, requestName } = params
2345
const baseUrl = params.baseUrl ?? env.clientEnv.NEXT_PUBLIC_CODEBUFF_APP_URL
2446
const apiKey = params.apiKey ?? env.ciEnv.CODEBUFF_API_KEY
2547

2648
if (!baseUrl || !apiKey) {
2749
return { error: 'Missing Codebuff base URL or API key' }
2850
}
2951

30-
const url = `${baseUrl}/api/v1/web-search`
31-
const payload = { query, depth, ...(repoUrl ? { repoUrl } : {}) }
52+
const url = `${baseUrl}${endpoint}`
3253

3354
try {
3455
const res = await withTimeout(
@@ -45,40 +66,27 @@ export async function callWebSearchAPI(params: {
4566
)
4667

4768
const text = await res.text()
48-
const tryJson = () => {
49-
try {
50-
return JSON.parse(text)
51-
} catch {
52-
return null
53-
}
54-
}
69+
const json = tryParseJson(text)
5570

5671
if (!res.ok) {
57-
const maybe = tryJson()
5872
const err =
59-
(maybe && (maybe.error || maybe.message)) || text || 'Request failed'
73+
getStringField(json, 'error') ??
74+
getStringField(json, 'message') ??
75+
text ??
76+
'Request failed'
6077
logger.warn(
6178
{
6279
url,
6380
status: res.status,
6481
statusText: res.statusText,
6582
body: text?.slice(0, 500),
6683
},
67-
'Web API web-search request failed',
84+
`Web API ${requestName} request failed`,
6885
)
69-
return { error: typeof err === 'string' ? err : 'Unknown error' }
86+
return { error: err }
7087
}
7188

72-
const data = tryJson()
73-
if (data && typeof data.result === 'string') {
74-
return {
75-
result: data.result,
76-
creditsUsed:
77-
typeof data.creditsUsed === 'number' ? data.creditsUsed : undefined,
78-
}
79-
}
80-
if (data && typeof data.error === 'string') return { error: data.error }
81-
return { error: 'Invalid response format' }
89+
return { json, creditsUsed: getNumberField(json, 'creditsUsed') }
8290
} catch (error) {
8391
logger.error(
8492
{
@@ -87,12 +95,46 @@ export async function callWebSearchAPI(params: {
8795
? { name: error.name, message: error.message, stack: error.stack }
8896
: error,
8997
},
90-
'Web API web-search network error',
98+
`Web API ${requestName} network error`,
9199
)
92100
return { error: error instanceof Error ? error.message : 'Network error' }
93101
}
94102
}
95103

104+
export async function callWebSearchAPI(params: {
105+
query: string
106+
depth?: 'standard' | 'deep'
107+
repoUrl?: string | null
108+
fetch: typeof globalThis.fetch
109+
logger: Logger
110+
env: CodebuffWebApiEnv
111+
baseUrl?: string
112+
apiKey?: string
113+
}): Promise<{ result?: string; error?: string; creditsUsed?: number }> {
114+
const { query, depth = 'standard', repoUrl, fetch, logger, env } = params
115+
const payload = { query, depth, ...(repoUrl ? { repoUrl } : {}) }
116+
117+
const res = await callCodebuffV1({
118+
endpoint: '/api/v1/web-search',
119+
payload,
120+
fetch,
121+
logger,
122+
env,
123+
baseUrl: params.baseUrl,
124+
apiKey: params.apiKey,
125+
requestName: 'web-search',
126+
})
127+
if (res.error) return { error: res.error }
128+
129+
const result = getStringField(res.json, 'result')
130+
if (result) {
131+
return { result, creditsUsed: res.creditsUsed }
132+
}
133+
134+
const error = getStringField(res.json, 'error')
135+
return { error: error ?? 'Invalid response format' }
136+
}
137+
96138
export async function callDocsSearchAPI(params: {
97139
libraryTitle: string
98140
topic?: string
@@ -105,78 +147,28 @@ export async function callDocsSearchAPI(params: {
105147
apiKey?: string
106148
}): Promise<{ documentation?: string; error?: string; creditsUsed?: number }> {
107149
const { libraryTitle, topic, maxTokens, repoUrl, fetch, logger, env } = params
108-
const baseUrl = params.baseUrl ?? env.clientEnv.NEXT_PUBLIC_CODEBUFF_APP_URL
109-
const apiKey = params.apiKey ?? env.ciEnv.CODEBUFF_API_KEY
110-
111-
if (!baseUrl || !apiKey) {
112-
return { error: 'Missing Codebuff base URL or API key' }
113-
}
114-
115-
const url = `${baseUrl}/api/v1/docs-search`
116-
const payload: Record<string, any> = { libraryTitle }
150+
const payload: Record<string, unknown> = { libraryTitle }
117151
if (topic) payload.topic = topic
118152
if (typeof maxTokens === 'number') payload.maxTokens = maxTokens
119153
if (repoUrl) payload.repoUrl = repoUrl
120154

121-
try {
122-
const res = await withTimeout(
123-
fetch(url, {
124-
method: 'POST',
125-
headers: {
126-
'Content-Type': 'application/json',
127-
Authorization: `Bearer ${apiKey}`,
128-
'x-codebuff-api-key': apiKey,
129-
},
130-
body: JSON.stringify(payload),
131-
}),
132-
FETCH_TIMEOUT_MS,
133-
)
134-
135-
const text = await res.text()
136-
const tryJson = () => {
137-
try {
138-
return JSON.parse(text) as any
139-
} catch {
140-
return null
141-
}
142-
}
143-
144-
if (!res.ok) {
145-
const maybe = tryJson()
146-
const err =
147-
(maybe && (maybe.error || maybe.message)) || text || 'Request failed'
148-
logger.warn(
149-
{
150-
url,
151-
status: res.status,
152-
statusText: res.statusText,
153-
body: text?.slice(0, 500),
154-
},
155-
'Web API docs-search request failed',
156-
)
157-
return { error: typeof err === 'string' ? err : 'Unknown error' }
158-
}
159-
160-
const data = tryJson()
161-
if (data && typeof data.documentation === 'string') {
162-
return {
163-
documentation: data.documentation,
164-
creditsUsed:
165-
typeof data.creditsUsed === 'number' ? data.creditsUsed : undefined,
166-
}
167-
}
168-
if (data && typeof data.error === 'string') return { error: data.error }
169-
return { error: 'Invalid response format' }
170-
} catch (error) {
171-
logger.error(
172-
{
173-
error:
174-
error instanceof Error
175-
? { name: error.name, message: error.message, stack: error.stack }
176-
: error,
177-
},
178-
'Web API docs-search network error',
179-
)
180-
return { error: error instanceof Error ? error.message : 'Network error' }
155+
const res = await callCodebuffV1({
156+
endpoint: '/api/v1/docs-search',
157+
payload,
158+
fetch,
159+
logger,
160+
env,
161+
baseUrl: params.baseUrl,
162+
apiKey: params.apiKey,
163+
requestName: 'docs-search',
164+
})
165+
if (res.error) return { error: res.error }
166+
167+
const documentation = getStringField(res.json, 'documentation')
168+
if (documentation) {
169+
return { documentation, creditsUsed: res.creditsUsed }
181170
}
171+
172+
const error = getStringField(res.json, 'error')
173+
return { error: error ?? 'Invalid response format' }
182174
}

packages/agent-runtime/src/llm-api/linkup-api.ts

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { withTimeout } from '@codebuff/common/util/promise'
22

33
import type { Logger } from '@codebuff/common/types/contracts/logger'
44

5-
interface LinkupEnv {
5+
export interface LinkupEnv {
66
LINKUP_API_KEY: string
77
}
88

@@ -20,12 +20,14 @@ export interface LinkupSearchResponse {
2020
sources: LinkupSearchResult[]
2121
}
2222

23-
/**
24-
* Searches the web using Linkup API
25-
* @param query The search query
26-
* @param options Search options including depth and max results
27-
* @returns Array containing a single result with the sourced answer or null if the request fails
28-
*/
23+
const headersToRecord = (headers: Headers): Record<string, string> => {
24+
const record: Record<string, string> = {}
25+
headers.forEach((value, key) => {
26+
record[key] = value
27+
})
28+
return record
29+
}
30+
2931
export async function searchWeb(options: {
3032
query: string
3133
depth?: 'standard' | 'deep'
@@ -57,7 +59,7 @@ export async function searchWeb(options: {
5759
try {
5860
const fetchStartTime = Date.now()
5961
const response = await withTimeout(
60-
fetch(`${LINKUP_API_BASE_URL}/search`, {
62+
fetch(requestUrl, {
6163
method: 'POST',
6264
headers: {
6365
'Content-Type': 'application/json',
@@ -92,26 +94,19 @@ export async function searchWeb(options: {
9294
responseBody: responseBody.substring(0, 500), // Truncate long responses
9395
fetchDuration,
9496
totalDuration: Date.now() - apiStartTime,
95-
headers: response.headers
96-
? (() => {
97-
const headerObj: Record<string, string> = {}
98-
response.headers.forEach((value, key) => {
99-
headerObj[key] = value
100-
})
101-
return headerObj
102-
})()
103-
: 'No headers',
97+
headers: headersToRecord(response.headers),
10498
},
10599
`Request failed with ${response.status}: ${response.statusText}`,
106100
)
107101
return null
108102
}
109103

110104
let data: LinkupSearchResponse
105+
let parseDuration = 0
111106
try {
112107
const parseStartTime = Date.now()
113108
data = (await response.json()) as LinkupSearchResponse
114-
const parseDuration = Date.now() - parseStartTime
109+
parseDuration = Date.now() - parseStartTime
115110
} catch (jsonError) {
116111
logger.error(
117112
{
@@ -124,6 +119,7 @@ export async function searchWeb(options: {
124119
}
125120
: jsonError,
126121
fetchDuration,
122+
parseDuration,
127123
totalDuration: Date.now() - apiStartTime,
128124
status: response.status,
129125
statusText: response.statusText,
@@ -142,6 +138,7 @@ export async function searchWeb(options: {
142138
answerLength: data?.answer?.length || 0,
143139
sourcesCount: data?.sources?.length || 0,
144140
fetchDuration,
141+
parseDuration,
145142
totalDuration: Date.now() - apiStartTime,
146143
},
147144
'Invalid response format - missing or invalid answer field',
@@ -156,6 +153,7 @@ export async function searchWeb(options: {
156153
answerLength: data.answer.length,
157154
sourcesCount: data.sources?.length || 0,
158155
fetchDuration,
156+
parseDuration,
159157
totalDuration,
160158
success: true,
161159
},

0 commit comments

Comments
 (0)