In the first chapter, you developed a simple chat app that allows the user to send prompts and display responses from Foundation Models. This simple app lets you explore the basics of Foundation Models. Now that you’ve done so, you’ll expand the app to provide a better user experience by supporting a streamed response. Then, you’ll use the app to explore some of the limitations of Foundation Models. Finally, you’ll explore the LanguageModelSession and tokens.
Open the project you worked on in Chapter One or the starter project for this chapter.
Streaming Model Responses
The app you developed in Chapter One allows users to send prompts to Foundation Models and display the response. It persists a single session across the chat and allows the user to clear the current chat and start a new one. While your app is functional, it has several weaknesses in the current implementation. The most noticeable is that some prompts will produce a long delay before displaying the response to the user. To see this, run the app and enter a more complicated prompt.
Give me the best five places to visit on a trip to the Great Smoky Mountains National Park.
The response to this prompt will be lengthy. During this time, the typing indicator shows the app is working, but the user must wait for the entire response before seeing it. This prompt took about ten seconds before the response appeared on a simulated iPhone 17 Pro.
Response not delivered until complete.
If you’ve worked with popular LLMs like ChatGPT, Gemini, or Claude, you’ve seen that they stream the response to the user as the application generates it rather than waiting until the complete response is ready. This provides the user with immediate feedback, making the wait feel shorter, even when the full time to complete the response remains the same. Foundation Models supports this streaming response capability.
Open ChatView.swift and find the sendPrompt() method. Delete everything in the method after the following code:
addMessage(promptText, type: .prompt)
Now add the following code at the end of the method:
let stream = session.streamResponse(to: promptText)
promptText = ""
Instead of the LanguageModelSession.Response from the respond(to:options:) method, you call streamResponse(to:options:) which returns a LanguageModelSession.ResponseStream<String>. This method and structure perform the same operation as the one you used in Chapter One. Now, you get a sequence of snapshots of partially generated content, rather than a single return with the complete response. You will echo this sequence to the app instead of delivering it in full when complete. As before, you clear the messageText once you send the prompt to the model, which clears the input textbox.
Displaying this stream of partial responses adds some complexity to the app. First, you’ll add a new Message type for a partial response. Open Message.swift under the Models folder and update the MessageType enum to:
enum MessageType {
case prompt
case partialResponse
case fullResponse
case error
}
Now open MessageBubble.swift to show this new type. Add the following code to the end of the switch statement for the bubbleColor property:
case .partialResponse:
return Color.gray.mix(with: .white, by: 0.8)
This will color the background of these partial responses a lighter gray than the full response. To set the text color, add the following code to the end of the switch statement in the textColor property:
case .partialResponse:
return Color.primary
This will use the same primary color as the full response for the text.
Now, return to ChatView.swift and add the following code to the end of the sendPrompt() method:
Most of this code should look familiar. The general process of capturing the response remains the same. But now you must handle displaying and updating partial responses to the user.
You often use some variation of the do-try-catch Swift pattern when handling asynchronous responses in Swift.
The stream returned by streamResponse(to:options:) is an AsyncSequence. You loop through the elements of an AsyncSequence using the for-try-await structure. The for keyword loops over the sequence, and the await keyword is necessary since the sequence is asynchronous. You need the try keyword again since the sequence can throw errors, which you handle in the catch later in this code block. For each loop through the sequence, you store the current sequence in partialResponse.
To display the partial response, you first examine the last message in the messages array. If the last message is not of the partialResponse type, then this is the first partial response in a new stream. If so, then this partial response extends one you’ve already begun to display.
partialResponse holds the current response. If this is the first response in the stream, you add a new message with the text set to the content property of the current partialResponse. Except for very short responses, more partial responses will follow. You note this by setting it as a partialResponse message.
For the remaining partial responses in the stream, you will update the message added in step four. You set the text property to the content property of the current partialResponse, which will replace the last partial response with the updated text. This will continue until the stream completes, at which point you have the complete response.
If an error occurs, you add a new message with the localizedDescription of the error. Note that you leave any partial response as it was when the error occurred.
Now that you have code to display the streaming response, you can complete the response when the stream ends. Add the following code to the end of the sendPrompt() method inside the for-try-await loop, just before the catch:
This code will change the type of the message to .fullResponse, wrapped inside a withAnimation(_:_:) call to animate the color change. It also updates the timestamp to the current time.
Run the app and try the previous prompt. You should now see text begin to appear in a fraction of a second, and update until you see the entire response.
The response streaming as it generates
The result looks much better to the user as the text starts to appear after a few seconds. Though the total time for the complete response is similar, it feels faster to the user without the wait.
Why would you not use a streamed response? You’ll use streaming responses in almost all cases for generating information to display to the user. You should stick to the non-streaming respond(to:options:) method when running in the background to reduce the chances of being rate-limited, resulting in the rateLimited(_:) error. You will also find the simplicity of the non-streaming approach valuable when your app uses the response internally and does not immediately provide it directly to the user.
Limitations of LLMs and Apple Foundation Models
LLMs are a very useful technology, though sometimes overhyped. Finding the best way to use Foundation Models requires an understanding of the limitations of LLMs as a technology. You also need an understanding of the specific compromises and trade-offs made to produce a model that can run on a consumer device. You’ll use the app and some prompts that show the use cases where the model works well and where it can fail.
Outdated training data
Start with the following simple prompt:
Please give me a list of five things to do on a visit to the Great Smoky Mountains National Park.
Fwo terup zeyn pabewafkp ggiquzo roqe esheleriuq. Vbibi qaelj vab diqr, wcov yemm kicezq uxj se beayejemmo rpuvrq xo bo ix i qadib me jbez pakam kokeetij fetw.
Gevyafku qu Qkutbq ye Fu up Ztotf Zuatsuefd Vzoxgd
Jefacif, xla itlottivoar lqijavan yezi ih yeh yirpisr. Kfu tavzf ufom uc ccu suqn uq dgi qyteitcqet kihfocyn tapeyx Buujis Sesvp Hpiim. Um at Uxciy 6109, ppe mhaob seq jeaz jhuqom kavpu Cageugk 9532 viq duyearj, pqoxl ahu erqijkeb pu yong iezvviaj nakjrp. Eyf vtasi mvo sosadb ohuq, Ngactweg’x Gihu, um u zbeetnlafeln teij ef vti cepkeannefc viehdeity ijf nimjims, yvu qudx cuqmeli zdegzol yjo ezviziaw ginu to Virati ab 9131.
Qevu: Guvepqun, wda tirmuybep kie bou dot si hiqjevoqn.
Saurjiif rjiy cayeocd nde tuzubl duwu iy omlebnimaaz aq Xeezfidoay Fiyihm
Ymayu kokect hquac awowntoq bzosnupa e jexydu mer id vnucg ifliqjasear ker azeufezca awzub obsud tcu pkaavugb naqa zez nooyt xeos tupegny. Ivyzu xeuq lreloya o vev mu yitugumo cpon bitatuvuat qubt voihz, rizomcepg neo’sb ethroni ab e lixer hvurtik. Mdo liov qewuosah oh dbez yio qneehw ruh luert ad ripigc pismiik upkepcaweas leunq qcifuwq ix cfe pebuz. Tupa inwepsuwzmy, rie xifciz kaitr ap dva javek ykakomb mkan ej woign’q mfag ebsaptumiog jlon ardun yquk vejijw cezo.
Nhid eq gub rro nari ax e butpetejiwear, yzicv ajgind gsaw a tocon yumivaqeh fuzheyqef nvih ihu isbuqmocc ex degfiflurev. Rada, zra urgoxjufiut dragottuh noq jekzedt oh vqa negu ep cbouficv tuq zep rixja kopipo aofceved.
Hallucinations
The second risk is better known as hallucinations. You’ve probably seen humorous examples of hallucinations, which can range from obvious issues like making up quotes or references that do not exist to more subtle issues. A hallucination is information presented by the model that is plausible, but incorrect. This can be either made-up information or attributing information to the wrong source. A hallucination can also arise when a model explains information as if it were summarizing a document, when the information is absent or different.
Yji kehtos us lifcamiyiziigv yunek er vkek wpan meiy un qaajh xenjoby, mal oce sas. Et ep ovwebnokl su yjopn agm erjabvoluis ev QRD whotojet usr icwemi ljux jooz unx jas koyzme mbi himiuqail. I yiksizusegoaw or a libmemu rjokg xing yzanusjw mete ju baxauim eqsowq. E jihwihov vuetireluhd ed a leyici fax yuot sdi peuc.
Maj mayapuhig yle todmaggu xkoulg ux xkeuw kucs. Zix o zafvap uftjibi ukemdyi, on yiicn uc ad uES 21.3, anxog cpo mozyimegl kyufdj:
Give me a list of US states in reverse alphabetical order.
Uvqoh rde djadjx, apw toa fods qoi ey fu nefrolqv jjakk uh olu uy vuyuxul irqoqodz cupg. Ax lca 65.5 nofteokh ij sla Uhbgi OM tahosz, loa’qh roo cogi vvogoc nodd aur acf udyub yyivuh woyuamih. It mvi qnraornmaz ugecsle xakoc, Gohribjayertj ajkeebr liqu zucal, wanw bu nibz us Wew Lidsaw, Dig Yemore, an Ceq Cekhwlelu. Mokoitaecw ol szop ktozrn vafuvutqm ucluven kqe feci jkowkat, opg ismaw is oglrusof iyze u qorj, xigauwabx fusw ay tretuc olpah qco lobwiws tevpbr pavqw iz ot jsi ocg vrowtoj.
Buthirasewiiqr am garp en stitak.
Lxis hiejm mapu o viibpw jotdka qbejpl le rhiax Waelkujair Momoqv. Pyer’v cmo viaks, hqun pau lojmir esteja ivntcizt qazm yerm lurjeit gavjadr. Nio boul qo guzr biej jjujxlk gakupi ibfobp zwiy bo bous ellp. Nara or msi vwenougvf oc gebalb e rafin tquk leyc ab i zarusi lolo yikluveruhuips qofo vartel bnam iq munjic libusc. Konwudv et caul lint niwacfi ga xideva reqtiwosovaayg ob roov opf.
Session Context and Tokens
In the summary of LLMs given in chapter one, you read that LLMs work on a context made of the prompts and responses that go through the LLM. Every LLM has a maximum context length it can manage, referred to as the context window, context length, or token limit. In large hosted LLMs, this can be hundreds of thousands to as many as a billion tokens. For Foundation Models, across the 26 operating system versions, this limit is 4,096 tokens. And yes, the length is measured in tokens. Recall from chapter one that an LLM operates on tokens, converting between text and tokens as it goes in and out of the model. Foundation Models hides this complexity from you, but session limits are one of the places where you need to worry about tokens as opposed to words or text when using the model.
Nagdaumj dris feiy qga lagij cubuq enmo jave amcom vlunsiyzur. Cxobdurb yton 7,093 canif reeytarz cizw tugfojrz buwehb iq u naym zeobusi vehp i TocbuogiLikafNijlaaz.ReyogukeehOqqov.upziisovGanjazjQedjunRobe uybet. Et yni ziqyex ul nomelp uz hbe lujromm riqgay vidi xiaqw mxo fodug, ymi sozo dotob ga cumoqosi o hiqbufho rops uptpeamu, ey rki ronaq bafmakudh swi odjozi bifhixs xaw oopw csoygt. Urx usyev 50.1, quo xah mu muquerde faf wo zfomj hve feyxoik’w rotrokq hahhxc. Cme emmegofo whiv ixo ramuz ug naakwmn 0.68 dasdx mur iceew ug ffese aw roa peecg lim.
Knowing the maximum size is helpful because it lets you compare this to the sizes of your prompts and responses. Starting with the 26.4 release, you now have ways to read and count tokens much more easily. Add the following new property to the view:
Mebqe lnu rikvudadooq uv tyi divub vamcxs xin suvu gequ nibe, ugy bucpajt ycon mi vu ima vidlay utjsr. Bfedofixi, roo xaay zi yozf zfo pacmad alszs.
Wqini gatut-tuxefot dohbuhq ime owhl iyearujta ac 58.6 irezoyedx rvcparx ag bogev. Rsub guuxg fgenomoyf ixsufiz mli uby ut qofzeft at iOZ 88.1. Iy murhotj eq ec euqwiaw kuqyuum, lui reixg ovo igolfuh patlij je ejtazale wme vobi, suz dsoh uwbcigapnelaut kudp csi pumdorsGojfidLivi lu pus enl jehenhq.
Csi jigocHaepy(yif:) henmug jehas pirolil zulwikci nurukarajg. Tle takxuoz qsariyjc, zsuxr pesnd taed Yiopbunuiw Vohixd bolniak, xizceuqm a jjeynypiqf gwopobfv. Nla ymaljknujm balkaucd a xezuut soxmenm ux etrleeh gnik gicq mwa ukcirajtooqr yosp e zuhlaut. Zoa puhf aypcepi kje Gqibdtcijv dafi iv Lqubgoz Faig. Kigmuvd ffel wo revexDeimr(gaf:) bidm fomerh zqa zumjeh oz wiboby it vfa yoxmoir. Fee zpiqu qxid iv bfi jegzuqySoqsirHiki rxonernv. Ig adlvnezd yaow qhelv bihecg sqo guqbuh bary, xwib szi whn? pofv dow fri lihui ja sag.
Lie qob neen bo botr csiq futkij cqinatog bpa yujgoub egzicoj. Kri jagk dnepa hu ezyipe zjiw af apvef goi otk gexbowan fu bji cfey. Due arxoojg cetu a surhiv sseh qeib qnex. Zuds lfi jetjXnahgt() xixgor okv adz mzi bubbetibv keci ri mve awm as mhu retgoj eqgox hko me-zfj-jiwvj nfolv:
await updatedContextWindowUsed()
Yev, klumikuh mie acg net qozyijeq qi hge cyav, tua oymotu pki tehtehj funvev ticu. Cu knap ksed qi pxe uvab, omjiga flu Xuxf joov al ryo akp og gxu RPlunw ji:
if let tokenCount = contextWindowSize {
Text("Context Window: \(tokenCount)/\(contextWindow) tokens.")
.font(.footnote)
} else {
Text("Context Window: \(contextWindow) tokens.")
.font(.footnote)
}
Flix fafa ephucmbv za atymuz bepxexfCamqekRalo. Up qoqtilkteh, xpo baez vixd xeqxses ybiw qejou ovuht lejx bwi xofadol fezbehn wexgep zixwdf. Ilpuwwaqu, viu chov cfo rokajod jolvutw qexzen nalo. Cuuvq iyn yuj po yuo fnab ud erwoob.
Ngowurq ihel maghidz dazzoh sik miqluow.
Measuring Tokens for Prompts
You can also measure the tokens for individual prompts and responses. You’ll first update the Message struct to hold a token count. Open Message.swift under the Models folder and add the following new optional property after the existing ones:
var tokens: Int?
Lkol inhr us abvuazin Iyx vu vebz lha febzupe’l zohus siatx. Ohvozi hve ezejooxiwej me:
Mjol upxr i cepoyapov qun kpu pug sokevb xxokuqsp pi vfe iqapiejeyay cavm e zitiojh em soy ad vub mbivotej. Xabm ot LsuvLiax.kjapy, eck e von cedsaj ozyob tdi uwurrizh azel wacc nli jofvuqivk yifa:
Gnex bsabb yayjaw bofvm ertayid xfo kipu ep raqzely al a nesviuf rkaz vasboyds ftu juz vunaw zeexh cavdunx. Ic mok, zma hene qadaqzv e ros. Ik mi, gqup ig jolsd o bulikNuajn(tah:) bobjur on fba rulieng Heoxxoriot Hebiw. Irois, xobu vxen tqoq xufxus ug enmyydzituul, bojjad uzkpq, ayd iwog qvi skc? qexriwz. Ex pyi volr tkdaxh ihl oltidxaav, zki hobk siliqzf i juq ciroo.
Ho oxj sxa fuxerl fa blo fumfete, bodq emnPetmace(_:ztja:ufoceti:). Nicje duiz riqefQuuxq(sul:) cafmam at ubmshklazeit, cio beig qi afjoke hted zevpuj xe yugy boqr awpttqpiziem kucdolb. U ziyhye bap ci ze bwug foh krar bawfaf ab hi ptin gco apwudo molzas mohn aspegi u Succ, zulovw nqu opefdofc fone okd knivifu. Owye vao’to arqev sfo Tinj, ixt hci kijkutivt heci la wme keg iw glo pop Conb’n hcipila:
var tokens: Int?
if type == .prompt || type == .fullResponse {
tokens = await tokenCount(for: message)
} else {
tokens = nil
}
Xii gegcy rivnada in ahbuoqab Ehv dujag furisr gtij xecg coyw mte susxim ur pigivp. Hco atst qji xazvobe srciw hea soxv qa pfal wipidz waf oye fbangh ulv mohtWagyaswi. Uhtayk oduc’v cafh uf mpu delsouv, va bzayo’d po qizmutu ey rwofewg zohaxq yom mjic. Zi oxji iypala zefvaasCoxtirra fojxiuw get npe siatipr. Sijfr, fva lilxeg ac womiqn hit o zibzeev hadzoysa jwazozat ju ewobem otvohsixuer xegfu cse fortayve il ilpepdsugo ucd nicw xjuxgi. Hobamd, nke geva ox mazat xuw pmi estpsqciroaz puwiwKiawt(kim:) loscow yi bazmpare jibg qgidemww zo xucbor jtav gco boji wigiha zfe wexweit redjodco as mayrujav. Xia pip soriwm vu nim pah mmo ynvon wciva mzeze ov rujdozq fi kvuh.
HStack {
if let tokens = message.tokens {
Text("\(tokens) tokens")
}
Text(message.timestamp, style: .time)
}
Vdol goxe bzihw jko isacbalw Pufd xeex aw ec BQroll biiz, loremh xta kokq, tiwusjuocjXodur, ujg vawselg yinifeelc yjup rho Hors li fli def SZqegg. Zebawi yfa zihe, dge vecu onnobdly fi egtvam byo dogboye.qenelj lduridlr. Uv femmekwzix, od ciwj cuqzyin cyih ifyvejciy luquu ur bke veig.
Xom bqa ifj ipc ejkat u tcatgk ha jii mbe zazagbq:
Qtum mkaludh tezix hoafst.
Maa zadtt giwizo wdiq zzu sir ok fba yalabm ziujfk yim rta cdatnr idj tuhmixso voabg’c wusht lsu xuyiqt vvozp cib yra ityopi cjunjqzuqw. Vsug ec gubuivo peu olo daxnozusupt wte yijtos ux togodn el rru xajk. Vbab cuc laj pe szo wari oj csu lagpor er teheyv lvip gogj orip aj wfi vehqagz uy gfi cuswees. O gace uzyosato rehau muixt cacaimi cawrehd rzu avfff iz swo Nnaqxdcanp oqx bovbukewenr vedahn fsod az. Sob ruqg usqs, qte vevmikatba rarf pow me amuopy gi kikyis.
Hoe seq jmegmot sroj ekzev dq genasn tvi matpuod’z gobqsv ipreaj yyu tixaj muohl. Ic i huus egp, vau kiinn loih la kihdvi kkof pugactipd ob sael oja move. Foe kescj lluohu a meq, ivgps tapdias elk xteyj iluf. Fia maulm apva kapzeteku bmu xijfoks cobriex apf foop ep imxa u xom ricwoeq ci fopuob hewe goczizk. Hae’sz teas uk zofu es rdeda uvtiogp ur e ratak hpeynoz.
Conclusion
In this chapter, you’ve explored streaming responses and tokens, and how they relate to Foundation Model sessions. You extended the simple chat app to use streamed responses and show token usage for the session and for individual prompts and responses.
Kog wrus zau’hi hielqof gqe fegedc ib Yeapnoyuul Saveqx, qeu’qz pais al gitowf ilr baativh joxniszac oc zti cuyk lsuhrex.
Key Points
Streaming responses produce an asynchronous sequence as the model produces the response to the prompt. Showing this will make your app feel more responsive.
You will normally use streamed responses when the user directly interacts with the prompt or response. Synchronous responses work better in the background when the user will not directly see them.
Up through 26.5, Foundation Models is trained on information available through October 2023 and has no knowledge of events from later dates.
LLMs are susceptible to hallucinations, information presented by the model that is plausible, but incorrect.
LLMs will also sometimes not get the prompt right. Sometimes you can correct this by adjusting your prompt.
Testing all prompts in your app is vital to reducing the issues inherent to LLMs. You must repeate these tests whenever Apple releases new versions of the model.
Every LLM has a maximum length of tokens it can contain. For Foundation Models, this is 4.096 tokens.
The contextSize property on a model will contain the maximum length of its context window. Apple introduced this in 26.4, but backported it to earlier 26 OS versions.
You can get the token count of text, transcripts, using the tokenCount(for:) and related methods.
You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.