11/**
22 * Wrappers around @mariozechner/pi-ai that fill capability gaps documented
33 * in docs/research/05-pi-ai-boundary.md. App code MUST go through this
4- * package — never import a provider SDK directly.
4+ * package - never import a provider SDK directly.
55 *
66 * Tier 1 implementations: minimum viable. Tier 2 features tracked separately.
77 */
@@ -21,6 +21,70 @@ export interface GenerateResult {
2121 costUsd : number ;
2222}
2323
24+ interface PiTextContent {
25+ type : 'text' ;
26+ text : string ;
27+ }
28+
29+ interface PiUsage {
30+ input : number ;
31+ output : number ;
32+ cacheRead : number ;
33+ cacheWrite : number ;
34+ totalTokens : number ;
35+ cost : {
36+ input : number ;
37+ output : number ;
38+ cacheRead : number ;
39+ cacheWrite : number ;
40+ total : number ;
41+ } ;
42+ }
43+
44+ interface PiUserMessage {
45+ role : 'user' ;
46+ content : string | PiTextContent [ ] ;
47+ timestamp : number ;
48+ }
49+
50+ interface PiAssistantMessage {
51+ role : 'assistant' ;
52+ content : Array < { type : string ; text ?: string } > ;
53+ api : string ;
54+ provider : string ;
55+ model : string ;
56+ usage : PiUsage ;
57+ stopReason : 'stop' | 'length' | 'toolUse' | 'error' | 'aborted' ;
58+ errorMessage ?: string ;
59+ timestamp : number ;
60+ }
61+
62+ interface PiContext {
63+ systemPrompt ?: string ;
64+ messages : Array < PiUserMessage | PiAssistantMessage > ;
65+ }
66+
67+ interface PiModel {
68+ id : string ;
69+ api : string ;
70+ provider : string ;
71+ }
72+
73+ const EMPTY_USAGE : PiUsage = {
74+ input : 0 ,
75+ output : 0 ,
76+ cacheRead : 0 ,
77+ cacheWrite : 0 ,
78+ totalTokens : 0 ,
79+ cost : {
80+ input : 0 ,
81+ output : 0 ,
82+ cacheRead : 0 ,
83+ cacheWrite : 0 ,
84+ total : 0 ,
85+ } ,
86+ } ;
87+
2488/**
2589 * Single non-streaming completion. Tier 1: thin shim, no caching, no retry.
2690 * Tier 2 will swap to pi-ai's streaming API and emit ArtifactEvents directly.
@@ -37,17 +101,12 @@ export async function complete(
37101 }
38102
39103 const pi = ( await import ( '@mariozechner/pi-ai' ) ) as unknown as {
40- getModel : ( provider : string , modelId : string ) => unknown ;
104+ getModel : ( provider : string , modelId : string ) => PiModel | undefined ;
41105 completeSimple : (
42- model : unknown ,
43- context : { messages : ChatMessage [ ] } ,
106+ model : PiModel ,
107+ context : PiContext ,
44108 opts : { apiKey : string ; baseUrl ?: string ; signal ?: AbortSignal } ,
45- ) => Promise < {
46- stopReason ?: string ;
47- errorMessage ?: string ;
48- content : Array < { type : string ; text ?: string } > ;
49- usage ?: { input ?: number ; output ?: number ; cost ?: { total ?: number } } ;
50- } > ;
109+ ) => Promise < PiAssistantMessage > ;
51110 } ;
52111
53112 const piModel = pi . getModel ( model . provider , model . modelId ) ;
@@ -64,7 +123,7 @@ export async function complete(
64123 if ( opts . baseUrl !== undefined ) piOpts . baseUrl = opts . baseUrl ;
65124 if ( opts . signal !== undefined ) piOpts . signal = opts . signal ;
66125
67- const result = await pi . completeSimple ( piModel , { messages } , piOpts ) ;
126+ const result = await pi . completeSimple ( piModel , toPiContext ( messages , piModel ) , piOpts ) ;
68127
69128 if ( result . stopReason === 'error' ) {
70129 throw new CodesignError ( result . errorMessage ?? 'Provider returned an error' , 'PROVIDER_ERROR' ) ;
@@ -83,6 +142,45 @@ export async function complete(
83142 } ;
84143}
85144
145+ function toPiContext ( messages : ChatMessage [ ] , model : PiModel ) : PiContext {
146+ const systemPrompt = messages
147+ . filter ( ( message ) => message . role === 'system' )
148+ . map ( ( message ) => message . content . trim ( ) )
149+ . filter ( ( content ) => content . length > 0 )
150+ . join ( '\n\n' ) ;
151+
152+ return {
153+ ...( systemPrompt . length > 0 ? { systemPrompt } : { } ) ,
154+ messages : messages . flatMap ( ( message , index ) => {
155+ const timestamp = index + 1 ;
156+
157+ if ( message . role === 'system' ) {
158+ return [ ] ;
159+ }
160+
161+ if ( message . role === 'user' ) {
162+ return {
163+ role : 'user' ,
164+ content : message . content ,
165+ timestamp,
166+ } ;
167+ }
168+
169+ return {
170+ role : 'assistant' ,
171+ content :
172+ message . content . trim ( ) . length === 0 ? [ ] : [ { type : 'text' , text : message . content } ] ,
173+ api : model . api ,
174+ provider : model . provider ,
175+ model : model . id ,
176+ usage : EMPTY_USAGE ,
177+ stopReason : 'stop' ,
178+ timestamp,
179+ } ;
180+ } ) ,
181+ } ;
182+ }
183+
86184/**
87185 * Detect API provider from a pasted key prefix. Used by the onboarding flow
88186 * to spare the user from picking a provider manually.
0 commit comments